mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 21:46:18 +01:00
Compare commits
473 Commits
v0.12.8
...
9f4e56c9cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4e56c9cf | ||
|
|
a3ebfbf37f | ||
|
|
cb90e96ae5 | ||
|
|
25ed5d0d66 | ||
|
|
3e5b91056e | ||
|
|
dc79f24c06 | ||
|
|
f9f5258b22 | ||
|
|
0b611cda57 | ||
|
|
74635e5763 | ||
|
|
8ce9088d9d | ||
|
|
b3331c00f8 | ||
|
|
d0559065c1 | ||
|
|
78f6006bdc | ||
|
|
436b42f4d1 | ||
|
|
16f7b30624 | ||
|
|
9249256c9f | ||
|
|
34163b8595 | ||
|
|
a4731f9179 | ||
|
|
9c74eccaf0 | ||
|
|
4768adf440 | ||
|
|
11ffb422e8 | ||
|
|
bab02ad738 | ||
|
|
61faee2450 | ||
|
|
d0be54f47c | ||
|
|
cc2be97055 | ||
|
|
2fc1565b75 | ||
|
|
f5421eff3c | ||
|
|
fc01ca1cad | ||
|
|
e2923126d2 | ||
|
|
972ce62ff5 | ||
|
|
05d9297ca3 | ||
|
|
ffc35b3c51 | ||
|
|
a547de8bf0 | ||
|
|
d8d1a89256 | ||
|
|
c29ba1c353 | ||
|
|
474d860929 | ||
|
|
fb57a57e77 | ||
|
|
02e5a8e9fc | ||
|
|
c0f6f64aa7 | ||
|
|
3077ed045d | ||
|
|
f7b62a2868 | ||
|
|
5daa0d3576 | ||
|
|
a6b9fa2aa9 | ||
|
|
5195e6d675 | ||
|
|
93e71dcf30 | ||
|
|
826227f3af | ||
|
|
cfe8645c18 | ||
|
|
681184b444 | ||
|
|
6d759cbe9f | ||
|
|
75480f66fa | ||
|
|
416c237ef3 | ||
|
|
ff64ac7a37 | ||
|
|
555f668b54 | ||
|
|
0dedc634a7 | ||
|
|
5500e45951 | ||
|
|
e7574a927f | ||
|
|
83fbaa7a3f | ||
|
|
cf3efa1f9f | ||
|
|
24093e33a9 | ||
|
|
075fad1da4 | ||
|
|
a35631415a | ||
|
|
8e99d67174 | ||
|
|
cf37c9a93c | ||
|
|
402d1d9fec | ||
|
|
b4f2afa4b6 | ||
|
|
beff2eb43f | ||
|
|
2e0d12a02d | ||
|
|
bc2fd34ac5 | ||
|
|
333cfae109 | ||
|
|
8cf8dd492d | ||
|
|
9b664b6400 | ||
|
|
c30d2fbe39 | ||
|
|
f1a4fae659 | ||
|
|
32675e403f | ||
|
|
d92d67eece | ||
|
|
ed1fc63ce2 | ||
|
|
0e2b1675fa | ||
|
|
ee8f901918 | ||
|
|
7dc2a86b1e | ||
|
|
51699ddc12 | ||
|
|
e0675567b8 | ||
|
|
61b3102eda | ||
|
|
7cc8f2b933 | ||
|
|
d38e3eab9c | ||
|
|
4ceb06b0c5 | ||
|
|
b1a468a0ab | ||
|
|
c6755183a8 | ||
|
|
cdc9f11ac0 | ||
|
|
6ccaaee57e | ||
|
|
214ee4a75a | ||
|
|
a506d3c84a | ||
|
|
812e849769 | ||
|
|
774eef1f3f | ||
|
|
fc85c50f2f | ||
|
|
fad22eee61 | ||
|
|
e84dbd639b | ||
|
|
2dd59b4e11 | ||
|
|
bb649971dc | ||
|
|
3f82ee0330 | ||
|
|
a26fde66e6 | ||
|
|
ed9b576bde | ||
|
|
aea463f5da | ||
|
|
45e00c70ab | ||
|
|
7fc4655f13 | ||
|
|
7dcaeaa2b4 | ||
|
|
d07293bf1a | ||
|
|
38298bbeab | ||
|
|
0124ccfac1 | ||
|
|
5f62ebcd7b | ||
|
|
ac354e9d6b | ||
|
|
5572cad7f6 | ||
|
|
5fa6a7c4e4 | ||
|
|
0f9442eaf8 | ||
|
|
6094d8d92d | ||
|
|
1469710166 | ||
|
|
c1df7edddc | ||
|
|
e70f06285c | ||
|
|
c5efa9b20c | ||
|
|
29a948ece4 | ||
|
|
d9b587c67b | ||
|
|
f806ae58b6 | ||
|
|
8b5dd8dedd | ||
|
|
222e5addef | ||
|
|
21e7bae720 | ||
|
|
a13d90d794 | ||
|
|
6e900d0f26 | ||
|
|
112f1853ee | ||
|
|
7aa9e3a6d3 | ||
|
|
6e9bc6a53b | ||
|
|
12eb884b1e | ||
|
|
270afa1c00 | ||
|
|
35505f2d50 | ||
|
|
5a28ba3a74 | ||
|
|
08d9126883 | ||
|
|
e9829229b6 | ||
|
|
6d7bb7ceee | ||
|
|
60a8b06b72 | ||
|
|
c4a145836b | ||
|
|
ea37451c98 | ||
|
|
1faad84cc2 | ||
|
|
d0b6200c5b | ||
|
|
fb66760665 | ||
|
|
480f1596bb | ||
|
|
6c11e2954e | ||
|
|
6a5f6530ef | ||
|
|
a4ee88bf7f | ||
|
|
d9f1f06b15 | ||
|
|
4c00988a37 | ||
|
|
193fbe9d31 | ||
|
|
ca6b6394ef | ||
|
|
1600b94846 | ||
|
|
b5aa66224a | ||
|
|
5a14eafae5 | ||
|
|
0b11dcdb1b | ||
|
|
fc1c135e71 | ||
|
|
7cb6966335 | ||
|
|
c090cf9e3e | ||
|
|
224bce7616 | ||
|
|
7fc3afb82b | ||
|
|
affdd66065 | ||
|
|
466c5a237b | ||
|
|
827758c97f | ||
|
|
fb4a35b054 | ||
|
|
ba561ec34c | ||
|
|
a94f85794c | ||
|
|
aa408f82c6 | ||
|
|
e8045a3438 | ||
|
|
7afd678f54 | ||
|
|
77e2b98470 | ||
|
|
70e894caf4 | ||
|
|
433bd6dde1 | ||
|
|
f60ee6a839 | ||
|
|
56e6dbf0a8 | ||
|
|
7ae179764d | ||
|
|
e54cac2c7b | ||
|
|
6bc8878408 | ||
|
|
0f6d85f124 | ||
|
|
b6717c11ae | ||
|
|
3cff9ccff8 | ||
|
|
3fb5f065b8 | ||
|
|
73e397abbf | ||
|
|
38f63b02bd | ||
|
|
879e84bb34 | ||
|
|
79b709d53c | ||
|
|
97e188a619 | ||
|
|
9459b59b14 | ||
|
|
c281a0717f | ||
|
|
b274261d3e | ||
|
|
63a78f2829 | ||
|
|
31aa6df5b2 | ||
|
|
74d8f685bd | ||
|
|
43a32ef0ce | ||
|
|
4426b53f47 | ||
|
|
6902e90ca7 | ||
|
|
9aeb80ce88 | ||
|
|
60c557055f | ||
|
|
0a6020b6b8 | ||
|
|
7f85b8f2a9 | ||
|
|
5ca388d855 | ||
|
|
34d74b3bcd | ||
|
|
86ff86aa99 | ||
|
|
d9a463e465 | ||
|
|
a5d0690a81 | ||
|
|
eef4092b16 | ||
|
|
a4e89676df | ||
|
|
3f083686fe | ||
|
|
cf2ccc1eb4 | ||
|
|
43c44a085b | ||
|
|
e90dfde12e | ||
|
|
a1bb77f4f2 | ||
|
|
ef582ec1ef | ||
|
|
2c30bdb2e4 | ||
|
|
5bfbc0420a | ||
|
|
b7a3222d31 | ||
|
|
d4955af3ba | ||
|
|
bde88dda26 | ||
|
|
3f51bd5ec6 | ||
|
|
092e8c9948 | ||
|
|
c35762de98 | ||
|
|
7c43e9e27c | ||
|
|
ee07a0d181 | ||
|
|
3178587f20 | ||
|
|
b07a791d6a | ||
|
|
8b6918b4a5 | ||
|
|
c7733146b7 | ||
|
|
8f23bbd436 | ||
|
|
7ca3cca15d | ||
|
|
dd553ec7b6 | ||
|
|
1a8dd0ab32 | ||
|
|
48e0c1efbf | ||
|
|
8cdddc9f5e | ||
|
|
756c5eab3e | ||
|
|
cf8102d547 | ||
|
|
156a54f26c | ||
|
|
ad1e7772af | ||
|
|
62edf55a37 | ||
|
|
223b627619 | ||
|
|
d3bc1a6764 | ||
|
|
5157144504 | ||
|
|
cdb396408f | ||
|
|
ca7c68140a | ||
|
|
5af7afb970 | ||
|
|
fd053fc8e5 | ||
|
|
f1f01657c0 | ||
|
|
1c9f03c848 | ||
|
|
82b5ee0424 | ||
|
|
0885bf2ba4 | ||
|
|
d541b42bef | ||
|
|
29599cd59c | ||
|
|
5fff9bd3ac | ||
|
|
81b6198ee7 | ||
|
|
01f58a328e | ||
|
|
6ffef3c33b | ||
|
|
3d50d0cbba | ||
|
|
a8ec54f5a5 | ||
|
|
3c44f51671 | ||
|
|
449642fdd2 | ||
|
|
bb324258d6 | ||
|
|
693117724a | ||
|
|
008dd9d184 | ||
|
|
f171ec9932 | ||
|
|
9e6e1771d1 | ||
|
|
bde6264d11 | ||
|
|
ae61acbedd | ||
|
|
af392c8084 | ||
|
|
ce8d206004 | ||
|
|
5a15e7c048 | ||
|
|
3a360f3ede | ||
|
|
7cf2493af7 | ||
|
|
d71a0083bb | ||
|
|
6f6aeeb315 | ||
|
|
7845d25c83 | ||
|
|
3320707567 | ||
|
|
e0df2a1e60 | ||
|
|
881c0cd137 | ||
|
|
5bde9500b6 | ||
|
|
b43541ea60 | ||
|
|
339e443bca | ||
|
|
8643fb2fd5 | ||
|
|
8dcf03fb15 | ||
|
|
53c3b0c359 | ||
|
|
1701947b26 | ||
|
|
c8cb041855 | ||
|
|
032d06601e | ||
|
|
1507825c16 | ||
|
|
073fc308bb | ||
|
|
9d5aaaf989 | ||
|
|
0f6063ebe5 | ||
|
|
742c217b5f | ||
|
|
85589e1e07 | ||
|
|
6ceb58254b | ||
|
|
10e21993ce | ||
|
|
ccff653ef1 | ||
|
|
323705aced | ||
|
|
774ddaa726 | ||
|
|
e75ada4483 | ||
|
|
14e8b28b85 | ||
|
|
1f7f764fca | ||
|
|
2757e51040 | ||
|
|
1233e6bee6 | ||
|
|
e4619b303e | ||
|
|
3abb7a2a29 | ||
|
|
045c3cfdf8 | ||
|
|
4b5e1cc5fa | ||
|
|
7600a47d08 | ||
|
|
47827c09f6 | ||
|
|
c8c84ca0ad | ||
|
|
309860f9d0 | ||
|
|
2a76cf4a1f | ||
|
|
6d6b6891e1 | ||
|
|
bdd24b95d2 | ||
|
|
9ae2bee9e3 | ||
|
|
b2396de0d9 | ||
|
|
d85e3bc26f | ||
|
|
2a3220be5a | ||
|
|
92910faca0 | ||
|
|
d596474426 | ||
|
|
db471ea619 | ||
|
|
f6e30b1c9f | ||
|
|
dcc013330e | ||
|
|
d6feda8a91 | ||
|
|
310892d401 | ||
|
|
302e951bb9 | ||
|
|
6810270f51 | ||
|
|
1403f75781 | ||
|
|
660a7967f8 | ||
|
|
5ad420a6bc | ||
|
|
7403f67109 | ||
|
|
626b865c3b | ||
|
|
ebbddef0d9 | ||
|
|
a528ddfea3 | ||
|
|
9c6a4873b2 | ||
|
|
82e976ff0b | ||
|
|
5342f2cbbc | ||
|
|
c7838f744f | ||
|
|
74e41851cf | ||
|
|
f1342a305c | ||
|
|
7f926c687b | ||
|
|
496cc67390 | ||
|
|
e4b300bc71 | ||
|
|
cee20d701a | ||
|
|
0cd5f3696d | ||
|
|
3686df0f9d | ||
|
|
f58f555367 | ||
|
|
adfa14ccbe | ||
|
|
26a147e2e5 | ||
|
|
b9a74e1284 | ||
|
|
21d2b3ec7b | ||
|
|
69d94b0bf9 | ||
|
|
5e49fca60e | ||
|
|
9babff17d1 | ||
|
|
be86983f00 | ||
|
|
907bb4dc52 | ||
|
|
9a34a3700d | ||
|
|
fdb468abf4 | ||
|
|
0de0326778 | ||
|
|
30db58b94f | ||
|
|
8e40b1013b | ||
|
|
aa96521696 | ||
|
|
17f40d58ac | ||
|
|
bdcdda4e9c | ||
|
|
c36d57f962 | ||
|
|
542ac4bfc0 | ||
|
|
68a684f3d6 | ||
|
|
b9bcb372f7 | ||
|
|
3d94451124 | ||
|
|
0af952d66c | ||
|
|
e46bc1ee36 | ||
|
|
5d297be871 | ||
|
|
6c0bc90f96 | ||
|
|
db0b6f77e3 | ||
|
|
7f42ab68d2 | ||
|
|
c4f6e81c56 | ||
|
|
3bf595959b | ||
|
|
5af1e058b0 | ||
|
|
ec62d1597b | ||
|
|
fa06a2935b | ||
|
|
2cdd521a10 | ||
|
|
90ac853e4f | ||
|
|
007fe0c0af | ||
|
|
f3afcd351a | ||
|
|
8eb161171e | ||
|
|
413f829107 | ||
|
|
7f09474f33 | ||
|
|
28386c58db | ||
|
|
e79aae7925 | ||
|
|
fabadf998b | ||
|
|
2f0d158ed8 | ||
|
|
b5f08d4e4c | ||
|
|
fce10da7f6 | ||
|
|
d59937dba7 | ||
|
|
0eb2bfab12 | ||
|
|
bb4111671f | ||
|
|
5e50d791fd | ||
|
|
cfaf0712d6 | ||
|
|
9cb2d694fe | ||
|
|
9f4c6b30d8 | ||
|
|
b9055a5d22 | ||
|
|
aa0d1e7f61 | ||
|
|
35a8cb1d36 | ||
|
|
92ba8a0ca3 | ||
|
|
acbc02162f | ||
|
|
8da777b6f4 | ||
|
|
e62ec1d993 | ||
|
|
4fa11b4c79 | ||
|
|
c9dba873ee | ||
|
|
3e2e897f34 | ||
|
|
9658fba5aa | ||
|
|
1c00c39eac | ||
|
|
2d3f186c18 | ||
|
|
983a471e6f | ||
|
|
7a228f553c | ||
|
|
007dfa9519 | ||
|
|
f3a74b1f46 | ||
|
|
4a76b620e7 | ||
|
|
25210d031d | ||
|
|
87b55fd4cf | ||
|
|
b4430ac76f | ||
|
|
5f9aa78b72 | ||
|
|
7537f6bd5c | ||
|
|
83dee5e554 | ||
|
|
036b4495e6 | ||
|
|
31cd36fcc1 | ||
|
|
0a2aaf3260 | ||
|
|
6e2e90120a | ||
|
|
23f95d6ebd | ||
|
|
56dc1096d9 | ||
|
|
9dd203f85a | ||
|
|
adac9cf79d | ||
|
|
efd2ba04c5 | ||
|
|
a9055e216d | ||
|
|
f64130029b | ||
|
|
6d172bac82 | ||
|
|
354cba5690 | ||
|
|
e65cde9675 | ||
|
|
fbd2fbb6a6 | ||
|
|
01bf64083a | ||
|
|
103856121d | ||
|
|
8a798c7e3f | ||
|
|
beeec5c39e | ||
|
|
6d43045d79 | ||
|
|
88ea94f5b0 | ||
|
|
3b8d333f8e | ||
|
|
3a06982502 | ||
|
|
b820b46042 | ||
|
|
fab799f177 | ||
|
|
5eaf9b9157 | ||
|
|
1eed3c53c8 | ||
|
|
90729a7a95 | ||
|
|
d450f6df10 | ||
|
|
7aa2bcf761 | ||
|
|
a7a86f46c3 | ||
|
|
17e30aff60 | ||
|
|
66008e47f3 | ||
|
|
56788b1e5b | ||
|
|
b72371487a | ||
|
|
7656b4189e | ||
|
|
8e6731c102 | ||
|
|
e86fa40fe4 | ||
|
|
f0e728a1ed | ||
|
|
bb076eb439 | ||
|
|
2f0b16367a | ||
|
|
aa33124e18 | ||
|
|
42f404c80a | ||
|
|
5056fddd40 | ||
|
|
2296202ea1 | ||
|
|
c034e9b0fa | ||
|
|
2d45119a98 | ||
|
|
63be4f1ab5 | ||
|
|
6d2259100e | ||
|
|
142af6e7b6 | ||
|
|
5c1e009188 | ||
|
|
27b2cb84d6 |
2
Makefile
2
Makefile
@@ -77,7 +77,7 @@ dev-hub: export ENV=dev
|
||||
dev-hub:
|
||||
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./internal -type f -name '*.go' | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||
find ./internal/cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||
else \
|
||||
cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||
fi
|
||||
|
||||
@@ -20,8 +20,9 @@ func HasReadableBattery() bool {
|
||||
}
|
||||
haveCheckedBattery = true
|
||||
bat, err := battery.Get(0)
|
||||
systemHasBattery = err == nil && bat != nil && bat.Design != 0 && bat.Full != 0
|
||||
if !systemHasBattery {
|
||||
if err == nil && bat != nil {
|
||||
systemHasBattery = true
|
||||
} else {
|
||||
slog.Debug("No battery found", "err", err)
|
||||
}
|
||||
return systemHasBattery
|
||||
|
||||
@@ -85,7 +85,7 @@ func getToken() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(tokenBytes)), nil
|
||||
return string(tokenBytes), nil
|
||||
}
|
||||
|
||||
// getOptions returns the WebSocket client options, creating them if necessary.
|
||||
|
||||
@@ -537,25 +537,4 @@ func TestGetToken(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", token, "Empty file should return empty string")
|
||||
})
|
||||
|
||||
t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) {
|
||||
unsetEnvVars()
|
||||
|
||||
tokenWithWhitespace := " test-token-with-whitespace \n\t"
|
||||
expectedToken := "test-token-with-whitespace"
|
||||
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tokenFile.Name())
|
||||
|
||||
_, err = tokenFile.WriteString(tokenWithWhitespace)
|
||||
require.NoError(t, err)
|
||||
tokenFile.Close()
|
||||
|
||||
os.Setenv("TOKEN_FILE", tokenFile.Name())
|
||||
defer os.Unsetenv("TOKEN_FILE")
|
||||
|
||||
token, err := getToken()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Package deltatracker provides a tracker for calculating differences in numeric values over time.
|
||||
package deltatracker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// Numeric is a constraint that permits any integer or floating-point type.
|
||||
type Numeric interface {
|
||||
constraints.Integer | constraints.Float
|
||||
}
|
||||
|
||||
// DeltaTracker is a generic, thread-safe tracker for calculating differences
|
||||
// in numeric values over time.
|
||||
// K is the key type (e.g., int, string).
|
||||
// V is the value type (e.g., int, int64, float32, float64).
|
||||
type DeltaTracker[K comparable, V Numeric] struct {
|
||||
sync.RWMutex
|
||||
current map[K]V
|
||||
previous map[K]V
|
||||
}
|
||||
|
||||
// NewDeltaTracker creates a new generic tracker.
|
||||
func NewDeltaTracker[K comparable, V Numeric]() *DeltaTracker[K, V] {
|
||||
return &DeltaTracker[K, V]{
|
||||
current: make(map[K]V),
|
||||
previous: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Set records the current value for a given ID.
|
||||
func (t *DeltaTracker[K, V]) Set(id K, value V) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
t.current[id] = value
|
||||
}
|
||||
|
||||
// Deltas returns a map of all calculated deltas for the current interval.
|
||||
func (t *DeltaTracker[K, V]) Deltas() map[K]V {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
deltas := make(map[K]V)
|
||||
for id, currentVal := range t.current {
|
||||
if previousVal, ok := t.previous[id]; ok {
|
||||
deltas[id] = currentVal - previousVal
|
||||
} else {
|
||||
deltas[id] = 0
|
||||
}
|
||||
}
|
||||
return deltas
|
||||
}
|
||||
|
||||
// Delta returns the delta for a single key.
|
||||
// Returns 0 if the key doesn't exist or has no previous value.
|
||||
func (t *DeltaTracker[K, V]) Delta(id K) V {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
currentVal, currentOk := t.current[id]
|
||||
if !currentOk {
|
||||
return 0
|
||||
}
|
||||
|
||||
previousVal, previousOk := t.previous[id]
|
||||
if !previousOk {
|
||||
return 0
|
||||
}
|
||||
|
||||
return currentVal - previousVal
|
||||
}
|
||||
|
||||
// Cycle prepares the tracker for the next interval.
|
||||
func (t *DeltaTracker[K, V]) Cycle() {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
t.previous = t.current
|
||||
t.current = make(map[K]V)
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
package deltatracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleDeltaTracker() {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
tracker.Set("key1", 10)
|
||||
tracker.Set("key2", 20)
|
||||
tracker.Cycle()
|
||||
tracker.Set("key1", 15)
|
||||
tracker.Set("key2", 30)
|
||||
fmt.Println(tracker.Delta("key1"))
|
||||
fmt.Println(tracker.Delta("key2"))
|
||||
fmt.Println(tracker.Deltas())
|
||||
// Output: 5
|
||||
// 10
|
||||
// map[key1:5 key2:10]
|
||||
}
|
||||
|
||||
func TestNewDeltaTracker(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
assert.NotNil(t, tracker)
|
||||
assert.Empty(t, tracker.current)
|
||||
assert.Empty(t, tracker.previous)
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
tracker.Set("key1", 10)
|
||||
|
||||
tracker.RLock()
|
||||
defer tracker.RUnlock()
|
||||
|
||||
assert.Equal(t, 10, tracker.current["key1"])
|
||||
}
|
||||
|
||||
func TestDeltas(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
|
||||
// Test with no previous values
|
||||
tracker.Set("key1", 10)
|
||||
tracker.Set("key2", 20)
|
||||
|
||||
deltas := tracker.Deltas()
|
||||
assert.Equal(t, 0, deltas["key1"])
|
||||
assert.Equal(t, 0, deltas["key2"])
|
||||
|
||||
// Cycle to move current to previous
|
||||
tracker.Cycle()
|
||||
|
||||
// Set new values and check deltas
|
||||
tracker.Set("key1", 15) // Delta should be 5 (15-10)
|
||||
tracker.Set("key2", 25) // Delta should be 5 (25-20)
|
||||
tracker.Set("key3", 30) // New key, delta should be 0
|
||||
|
||||
deltas = tracker.Deltas()
|
||||
assert.Equal(t, 5, deltas["key1"])
|
||||
assert.Equal(t, 5, deltas["key2"])
|
||||
assert.Equal(t, 0, deltas["key3"])
|
||||
}
|
||||
|
||||
func TestCycle(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
|
||||
tracker.Set("key1", 10)
|
||||
tracker.Set("key2", 20)
|
||||
|
||||
// Verify current has values
|
||||
tracker.RLock()
|
||||
assert.Equal(t, 10, tracker.current["key1"])
|
||||
assert.Equal(t, 20, tracker.current["key2"])
|
||||
assert.Empty(t, tracker.previous)
|
||||
tracker.RUnlock()
|
||||
|
||||
tracker.Cycle()
|
||||
|
||||
// After cycle, previous should have the old current values
|
||||
// and current should be empty
|
||||
tracker.RLock()
|
||||
assert.Empty(t, tracker.current)
|
||||
assert.Equal(t, 10, tracker.previous["key1"])
|
||||
assert.Equal(t, 20, tracker.previous["key2"])
|
||||
tracker.RUnlock()
|
||||
}
|
||||
|
||||
func TestCompleteWorkflow(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
|
||||
// First interval
|
||||
tracker.Set("server1", 100)
|
||||
tracker.Set("server2", 200)
|
||||
|
||||
// Get deltas for first interval (should be zero)
|
||||
firstDeltas := tracker.Deltas()
|
||||
assert.Equal(t, 0, firstDeltas["server1"])
|
||||
assert.Equal(t, 0, firstDeltas["server2"])
|
||||
|
||||
// Cycle to next interval
|
||||
tracker.Cycle()
|
||||
|
||||
// Second interval
|
||||
tracker.Set("server1", 150) // Delta: 50
|
||||
tracker.Set("server2", 180) // Delta: -20
|
||||
tracker.Set("server3", 300) // New server, delta: 300
|
||||
|
||||
secondDeltas := tracker.Deltas()
|
||||
assert.Equal(t, 50, secondDeltas["server1"])
|
||||
assert.Equal(t, -20, secondDeltas["server2"])
|
||||
assert.Equal(t, 0, secondDeltas["server3"])
|
||||
}
|
||||
|
||||
func TestDeltaTrackerWithDifferentTypes(t *testing.T) {
|
||||
// Test with int64
|
||||
intTracker := NewDeltaTracker[string, int64]()
|
||||
intTracker.Set("pid1", 1000)
|
||||
intTracker.Cycle()
|
||||
intTracker.Set("pid1", 1200)
|
||||
intDeltas := intTracker.Deltas()
|
||||
assert.Equal(t, int64(200), intDeltas["pid1"])
|
||||
|
||||
// Test with float64
|
||||
floatTracker := NewDeltaTracker[string, float64]()
|
||||
floatTracker.Set("cpu1", 1.5)
|
||||
floatTracker.Cycle()
|
||||
floatTracker.Set("cpu1", 2.7)
|
||||
floatDeltas := floatTracker.Deltas()
|
||||
assert.InDelta(t, 1.2, floatDeltas["cpu1"], 0.0001)
|
||||
|
||||
// Test with int keys
|
||||
pidTracker := NewDeltaTracker[int, int64]()
|
||||
pidTracker.Set(101, 20000)
|
||||
pidTracker.Cycle()
|
||||
pidTracker.Set(101, 22500)
|
||||
pidDeltas := pidTracker.Deltas()
|
||||
assert.Equal(t, int64(2500), pidDeltas[101])
|
||||
}
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
|
||||
// Test getting delta for non-existent key
|
||||
result := tracker.Delta("nonexistent")
|
||||
assert.Equal(t, 0, result)
|
||||
|
||||
// Test getting delta for key with no previous value
|
||||
tracker.Set("key1", 10)
|
||||
result = tracker.Delta("key1")
|
||||
assert.Equal(t, 0, result)
|
||||
|
||||
// Cycle to move current to previous
|
||||
tracker.Cycle()
|
||||
|
||||
// Test getting delta for key with previous value
|
||||
tracker.Set("key1", 15)
|
||||
result = tracker.Delta("key1")
|
||||
assert.Equal(t, 5, result)
|
||||
|
||||
// Test getting delta for key that exists in previous but not current
|
||||
result = tracker.Delta("key1")
|
||||
assert.Equal(t, 5, result) // Should still return 5
|
||||
|
||||
// Test getting delta for key that exists in current but not previous
|
||||
tracker.Set("key2", 20)
|
||||
result = tracker.Delta("key2")
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
func TestDeltaWithDifferentTypes(t *testing.T) {
|
||||
// Test with int64
|
||||
intTracker := NewDeltaTracker[string, int64]()
|
||||
intTracker.Set("pid1", 1000)
|
||||
intTracker.Cycle()
|
||||
intTracker.Set("pid1", 1200)
|
||||
result := intTracker.Delta("pid1")
|
||||
assert.Equal(t, int64(200), result)
|
||||
|
||||
// Test with float64
|
||||
floatTracker := NewDeltaTracker[string, float64]()
|
||||
floatTracker.Set("cpu1", 1.5)
|
||||
floatTracker.Cycle()
|
||||
floatTracker.Set("cpu1", 2.7)
|
||||
floatResult := floatTracker.Delta("cpu1")
|
||||
assert.InDelta(t, 1.2, floatResult, 0.0001)
|
||||
|
||||
// Test with int keys
|
||||
pidTracker := NewDeltaTracker[int, int64]()
|
||||
pidTracker.Set(101, 20000)
|
||||
pidTracker.Cycle()
|
||||
pidTracker.Set(101, 22500)
|
||||
pidResult := pidTracker.Delta(101)
|
||||
assert.Equal(t, int64(2500), pidResult)
|
||||
}
|
||||
|
||||
func TestDeltaConcurrentAccess(t *testing.T) {
|
||||
tracker := NewDeltaTracker[string, int]()
|
||||
|
||||
// Set initial values
|
||||
tracker.Set("key1", 10)
|
||||
tracker.Set("key2", 20)
|
||||
tracker.Cycle()
|
||||
|
||||
// Set new values
|
||||
tracker.Set("key1", 15)
|
||||
tracker.Set("key2", 25)
|
||||
|
||||
// Test concurrent access safety
|
||||
result1 := tracker.Delta("key1")
|
||||
result2 := tracker.Delta("key2")
|
||||
|
||||
assert.Equal(t, 5, result1)
|
||||
assert.Equal(t, 5, result2)
|
||||
}
|
||||
@@ -1,83 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func (a *Agent) updateNetworkStats(systemStats *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 systemStats.NetworkInterfaces == nil {
|
||||
systemStats.NetworkInterfaces = make(map[string][4]uint64, 0)
|
||||
}
|
||||
|
||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
|
||||
a.netIoStats.Time = time.Now()
|
||||
totalBytesSent := uint64(0)
|
||||
totalBytesRecv := uint64(0)
|
||||
netInterfaceDeltaTracker.Cycle()
|
||||
// sum all bytes sent and received
|
||||
for _, v := range netIO {
|
||||
// skip if not in valid network interfaces list
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
}
|
||||
totalBytesSent += v.BytesSent
|
||||
totalBytesRecv += v.BytesRecv
|
||||
|
||||
// track deltas for each network interface
|
||||
netInterfaceDeltaTracker.Set(fmt.Sprintf("%sdown", v.Name), v.BytesRecv)
|
||||
netInterfaceDeltaTracker.Set(fmt.Sprintf("%sup", v.Name), v.BytesSent)
|
||||
upDelta := netInterfaceDeltaTracker.Delta(fmt.Sprintf("%sup", v.Name)) * 1000 / msElapsed
|
||||
downDelta := netInterfaceDeltaTracker.Delta(fmt.Sprintf("%sdown", v.Name)) * 1000 / msElapsed
|
||||
// add interface to systemStats
|
||||
systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}
|
||||
}
|
||||
|
||||
// add to systemStats
|
||||
var bytesSentPerSecond, bytesRecvPerSecond uint64
|
||||
if msElapsed > 0 {
|
||||
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
|
||||
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
|
||||
}
|
||||
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
|
||||
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
|
||||
// add check for issue (#150) where sent is a massive number
|
||||
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
||||
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
|
||||
for _, v := range netIO {
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
}
|
||||
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
|
||||
}
|
||||
// reset network I/O stats
|
||||
a.initializeNetIoStats()
|
||||
} else {
|
||||
systemStats.NetworkSent = networkSentPs
|
||||
systemStats.NetworkRecv = networkRecvPs
|
||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
||||
// update netIoStats
|
||||
a.netIoStats.BytesSent = totalBytesSent
|
||||
a.netIoStats.BytesRecv = totalBytesRecv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) initializeNetIoStats() {
|
||||
// reset valid network interfaces
|
||||
a.netInterfaces = make(map[string]struct{}, 0)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/agent/battery"
|
||||
"github.com/henrygd/beszel/agent/deltatracker"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
@@ -19,10 +18,9 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]()
|
||||
|
||||
// Sets initial / non-changing values about the host system
|
||||
func (a *Agent) initializeSystemInfo() {
|
||||
a.systemInfo.AgentVersion = beszel.Version
|
||||
@@ -72,7 +70,7 @@ func (a *Agent) initializeSystemInfo() {
|
||||
|
||||
// Returns current info, stats about the host system
|
||||
func (a *Agent) getSystemStats() system.Stats {
|
||||
var systemStats system.Stats
|
||||
systemStats := system.Stats{}
|
||||
|
||||
// battery
|
||||
if battery.HasReadableBattery() {
|
||||
@@ -175,7 +173,55 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
}
|
||||
|
||||
// network stats
|
||||
a.updateNetworkStats(&systemStats)
|
||||
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 {
|
||||
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
|
||||
a.netIoStats.Time = time.Now()
|
||||
totalBytesSent := uint64(0)
|
||||
totalBytesRecv := uint64(0)
|
||||
// sum all bytes sent and received
|
||||
for _, v := range netIO {
|
||||
// skip if not in valid network interfaces list
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
}
|
||||
totalBytesSent += v.BytesSent
|
||||
totalBytesRecv += v.BytesRecv
|
||||
}
|
||||
// add to systemStats
|
||||
var bytesSentPerSecond, bytesRecvPerSecond uint64
|
||||
if msElapsed > 0 {
|
||||
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
|
||||
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
|
||||
}
|
||||
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
|
||||
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
|
||||
// add check for issue (#150) where sent is a massive number
|
||||
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
||||
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
|
||||
for _, v := range netIO {
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
}
|
||||
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
|
||||
}
|
||||
// reset network I/O stats
|
||||
a.initializeNetIoStats()
|
||||
} else {
|
||||
systemStats.NetworkSent = networkSentPs
|
||||
systemStats.NetworkRecv = networkRecvPs
|
||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
||||
// update netIoStats
|
||||
a.netIoStats.BytesSent = totalBytesSent
|
||||
a.netIoStats.BytesRecv = totalBytesRecv
|
||||
}
|
||||
}
|
||||
|
||||
// temperatures
|
||||
// TODO: maybe refactor to methods on systemStats
|
||||
|
||||
@@ -6,7 +6,7 @@ import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
// Version is the current version of the application.
|
||||
Version = "0.12.8"
|
||||
Version = "0.12.7"
|
||||
// AppName is the name of the application.
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
@@ -25,12 +25,7 @@ type alertInfo struct {
|
||||
// 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() {
|
||||
processPendingAlerts := time.Tick(15 * time.Second)
|
||||
|
||||
// check for status alerts that are not resolved when system comes up
|
||||
// (can be removed if we figure out core bug in #1052)
|
||||
checkStatusAlerts := time.Tick(561 * time.Second)
|
||||
|
||||
tick := time.Tick(15 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-am.stopChan:
|
||||
@@ -46,9 +41,7 @@ func (am *AlertManager) startWorker() {
|
||||
case "cancel":
|
||||
am.pendingAlerts.Delete(task.alertRecord.Id)
|
||||
}
|
||||
case <-checkStatusAlerts:
|
||||
resolveStatusAlerts(am.hub)
|
||||
case <-processPendingAlerts:
|
||||
case <-tick:
|
||||
// Check for expired alerts every tick
|
||||
now := time.Now()
|
||||
for key, value := range am.pendingAlerts.Range {
|
||||
@@ -177,35 +170,3 @@ func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, a
|
||||
LinkText: "View " + systemName,
|
||||
})
|
||||
}
|
||||
|
||||
// resolveStatusAlerts resolves any status alerts that weren't resolved
|
||||
// when system came up (https://github.com/henrygd/beszel/issues/1052)
|
||||
func resolveStatusAlerts(app core.App) error {
|
||||
db := app.DB()
|
||||
// Find all active status alerts where the system is actually up
|
||||
var alertIds []string
|
||||
err := db.NewQuery(`
|
||||
SELECT a.id
|
||||
FROM alerts a
|
||||
JOIN systems s ON a.system = s.id
|
||||
WHERE a.name = 'Status'
|
||||
AND a.triggered = true
|
||||
AND s.status = 'up'
|
||||
`).Column(&alertIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// resolve all matching alert records
|
||||
for _, alertId := range alertIds {
|
||||
alert, err := app.FindRecordById("alerts", alertId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alert.Set("triggered", false)
|
||||
err = app.Save(alert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/alerts"
|
||||
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -370,9 +369,33 @@ func TestUserAlertsApi(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getHubWithUser(t *testing.T) (*beszelTests.TestHub, *core.Record) {
|
||||
hub, err := beszelTests.NewTestHub(t.TempDir())
|
||||
assert.NoError(t, err)
|
||||
hub.StartHub()
|
||||
|
||||
// Manually initialize the system manager to bind event hooks
|
||||
err = hub.GetSystemManager().Initialize()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a test user
|
||||
user, err := beszelTests.CreateUser(hub, "test@example.com", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create user settings for the test user (required for alert notifications)
|
||||
userSettingsData := map[string]any{
|
||||
"user": user.Id,
|
||||
"settings": `{"emails":[test@example.com],"webhooks":[]}`,
|
||||
}
|
||||
_, err = beszelTests.CreateRecord(hub, "user_settings", userSettingsData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return hub, user
|
||||
}
|
||||
|
||||
func TestStatusAlerts(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
hub, user := beszelTests.GetHubWithUser(t)
|
||||
hub, user := getHubWithUser(t)
|
||||
defer hub.Cleanup()
|
||||
|
||||
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
|
||||
@@ -453,7 +476,7 @@ func TestStatusAlerts(t *testing.T) {
|
||||
|
||||
func TestAlertsHistory(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
hub, user := beszelTests.GetHubWithUser(t)
|
||||
hub, user := getHubWithUser(t)
|
||||
defer hub.Cleanup()
|
||||
|
||||
// Create systems and alerts
|
||||
@@ -579,102 +602,3 @@ func TestAlertsHistory(t *testing.T) {
|
||||
assert.EqualValues(t, 2, totalHistoryCount, "Should have 2 total alert history records")
|
||||
})
|
||||
}
|
||||
func TestResolveStatusAlerts(t *testing.T) {
|
||||
hub, user := beszelTests.GetHubWithUser(t)
|
||||
defer hub.Cleanup()
|
||||
|
||||
// Create a systemUp
|
||||
systemUp, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system",
|
||||
"users": []string{user.Id},
|
||||
"host": "127.0.0.1",
|
||||
"status": "up",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
systemDown, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system-2",
|
||||
"users": []string{user.Id},
|
||||
"host": "127.0.0.2",
|
||||
"status": "up",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a status alertUp for the system
|
||||
alertUp, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
|
||||
"name": "Status",
|
||||
"system": systemUp.Id,
|
||||
"user": user.Id,
|
||||
"min": 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
alertDown, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
|
||||
"name": "Status",
|
||||
"system": systemDown.Id,
|
||||
"user": user.Id,
|
||||
"min": 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify alert is not triggered initially
|
||||
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered initially")
|
||||
|
||||
// Set the system to 'up' (this should not trigger the alert)
|
||||
systemUp.Set("status", "up")
|
||||
err = hub.SaveNoValidate(systemUp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
systemDown.Set("status", "down")
|
||||
err = hub.SaveNoValidate(systemDown)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Wait a moment for any processing
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Verify alertUp is still not triggered after setting system to up
|
||||
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered when system is up")
|
||||
|
||||
// Manually set both alerts triggered to true
|
||||
alertUp.Set("triggered", true)
|
||||
err = hub.SaveNoValidate(alertUp)
|
||||
assert.NoError(t, err)
|
||||
alertDown.Set("triggered", true)
|
||||
err = hub.SaveNoValidate(alertDown)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify we have exactly one alert with triggered true
|
||||
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, triggeredCount, "Should have exactly two alerts with triggered true")
|
||||
|
||||
// Verify the specific alertUp is triggered
|
||||
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, alertUp.GetBool("triggered"), "Alert should be triggered")
|
||||
|
||||
// Verify we have two unresolved alert history records
|
||||
alertHistoryCount, err := hub.CountRecords("alerts_history", dbx.HashExp{"resolved": ""})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, alertHistoryCount, "Should have exactly two unresolved alert history records")
|
||||
|
||||
err = alerts.ResolveStatusAlerts(hub)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify alertUp is not triggered after resolving
|
||||
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered after resolving")
|
||||
// Verify alertDown is still triggered
|
||||
alertDown, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertDown.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, alertDown.GetBool("triggered"), "Alert should still be triggered after resolving")
|
||||
|
||||
// Verify we have one unresolved alert history record
|
||||
alertHistoryCount, err = hub.CountRecords("alerts_history", dbx.HashExp{"resolved": ""})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, alertHistoryCount, "Should have exactly one unresolved alert history record")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package alerts
|
||||
|
||||
import (
|
||||
@@ -56,7 +53,3 @@ func (am *AlertManager) ForceExpirePendingAlerts() {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func ResolveStatusAlerts(app core.App) error {
|
||||
return resolveStatusAlerts(app)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . ./
|
||||
COPY go.mod go.sum ./
|
||||
# RUN go mod download
|
||||
COPY *.go ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
||||
|
||||
# --------------------------
|
||||
# Final image: GPU-enabled agent with nvidia-smi
|
||||
@@ -21,7 +18,4 @@ RUN rm -rf /tmp/*
|
||||
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
# this is so we don't need to create the /tmp directory in the scratch container
|
||||
COPY --from=builder /tmp /tmp
|
||||
|
||||
ENTRYPOINT ["/agent"]
|
||||
|
||||
@@ -38,10 +38,9 @@ type Stats struct {
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
// TODO: remove other load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
||||
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
|
||||
@@ -175,31 +175,35 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
|
||||
// custom middlewares
|
||||
func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
||||
// authorizes request with user matching the provided email
|
||||
authorizeRequestWithEmail := func(e *core.RequestEvent, email string) (err error) {
|
||||
if e.Auth != nil || email == "" {
|
||||
return e.Next()
|
||||
}
|
||||
isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost
|
||||
e.Auth, err = e.App.FindFirstRecordByData("users", "email", email)
|
||||
if err != nil || !isAuthRefresh {
|
||||
return e.Next()
|
||||
}
|
||||
// auth refresh endpoint, make sure token is set in header
|
||||
token, _ := e.Auth.NewAuthToken()
|
||||
e.Request.Header.Set("Authorization", token)
|
||||
return e.Next()
|
||||
}
|
||||
// authenticate with trusted header
|
||||
if autoLogin, _ := GetEnv("AUTO_LOGIN"); autoLogin != "" {
|
||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||
return authorizeRequestWithEmail(e, autoLogin)
|
||||
})
|
||||
}
|
||||
// authenticate with trusted header
|
||||
if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
||||
if e.Auth != nil {
|
||||
return e.Next()
|
||||
}
|
||||
trustedEmail := e.Request.Header.Get(trustedHeader)
|
||||
if trustedEmail == "" {
|
||||
return e.Next()
|
||||
}
|
||||
isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost
|
||||
if !isAuthRefresh {
|
||||
authRecord, err := e.App.FindAuthRecordByEmail("users", trustedEmail)
|
||||
if err == nil {
|
||||
e.Auth = authRecord
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
// if auth refresh endpoint, find user record directly and generate token
|
||||
user, err := e.App.FindFirstRecordByData("users", "email", trustedEmail)
|
||||
if err != nil {
|
||||
return e.Next()
|
||||
}
|
||||
e.Auth = user
|
||||
// need to set the authorization header for the client sdk to pick up the token
|
||||
if token, err := user.NewAuthToken(); err == nil {
|
||||
e.Request.Header.Set("Authorization", token)
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,60 +712,6 @@ func TestCreateUserEndpointAvailability(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAutoLoginMiddleware(t *testing.T) {
|
||||
var hubs []*beszelTests.TestHub
|
||||
|
||||
defer func() {
|
||||
defer os.Unsetenv("AUTO_LOGIN")
|
||||
for _, hub := range hubs {
|
||||
hub.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("AUTO_LOGIN", "user@test.com")
|
||||
|
||||
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||
hubs = append(hubs, hub)
|
||||
hub.StartHub()
|
||||
return hub.TestApp
|
||||
}
|
||||
|
||||
scenarios := []beszelTests.ApiScenario{
|
||||
{
|
||||
Name: "GET /getkey - without auto login should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with auto login should fail if no matching user",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with auto login should succeed",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||
TestAppFactory: testAppFactory,
|
||||
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedHeaderMiddleware(t *testing.T) {
|
||||
var hubs []*beszelTests.TestHub
|
||||
|
||||
|
||||
@@ -225,19 +225,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])
|
||||
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
|
||||
|
||||
// Accumulate network interfaces
|
||||
if sum.NetworkInterfaces == nil {
|
||||
sum.NetworkInterfaces = make(map[string][4]uint64, len(stats.NetworkInterfaces))
|
||||
}
|
||||
for key, value := range stats.NetworkInterfaces {
|
||||
sum.NetworkInterfaces[key] = [4]uint64{
|
||||
sum.NetworkInterfaces[key][0] + value[0],
|
||||
sum.NetworkInterfaces[key][1] + value[1],
|
||||
max(sum.NetworkInterfaces[key][2], value[2]),
|
||||
max(sum.NetworkInterfaces[key][3], value[3]),
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate temperatures
|
||||
if stats.Temperatures != nil {
|
||||
if sum.Temperatures == nil {
|
||||
@@ -312,19 +299,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
|
||||
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
|
||||
sum.Battery[0] = uint8(batterySum / int(count))
|
||||
|
||||
// Average network interfaces
|
||||
if sum.NetworkInterfaces != nil {
|
||||
for key := range sum.NetworkInterfaces {
|
||||
sum.NetworkInterfaces[key] = [4]uint64{
|
||||
sum.NetworkInterfaces[key][0] / uint64(count),
|
||||
sum.NetworkInterfaces[key][1] / uint64(count),
|
||||
sum.NetworkInterfaces[key][2],
|
||||
sum.NetworkInterfaces[key][3],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Average temperatures
|
||||
if sum.Temperatures != nil && tempCount > 0 {
|
||||
for key := range sum.Temperatures {
|
||||
|
||||
@@ -175,7 +175,7 @@ func TestDeleteOldSystemStats(t *testing.T) {
|
||||
}
|
||||
|
||||
// Run deletion
|
||||
err = records.DeleteOldSystemStats(hub)
|
||||
err = records.TestDeleteOldSystemStats(hub)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
@@ -268,7 +268,7 @@ func TestDeleteOldAlertsHistory(t *testing.T) {
|
||||
assert.Equal(t, int64(tc.alertCount), countBefore, "Initial count should match")
|
||||
|
||||
// Run deletion
|
||||
err = records.DeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)
|
||||
err = records.TestDeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count after deletion
|
||||
@@ -332,7 +332,7 @@ func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should not error and should not delete anything
|
||||
err = records.DeleteOldAlertsHistory(hub, 10, 20)
|
||||
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := hub.CountRecords("alerts_history")
|
||||
@@ -346,7 +346,7 @@ func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not error with empty table
|
||||
err = records.DeleteOldAlertsHistory(hub, 10, 20)
|
||||
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func TestTwoDecimals(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := records.TwoDecimals(tc.input)
|
||||
result := records.TestTwoDecimals(tc.input)
|
||||
assert.InDelta(t, tc.expected, result, 0.02, "twoDecimals(%f) should equal %f", tc.input, tc.expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// DeleteOldSystemStats exposes deleteOldSystemStats for testing
|
||||
func DeleteOldSystemStats(app core.App) error {
|
||||
// TestDeleteOldSystemStats exposes deleteOldSystemStats for testing
|
||||
func TestDeleteOldSystemStats(app core.App) error {
|
||||
return deleteOldSystemStats(app)
|
||||
}
|
||||
|
||||
// DeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing
|
||||
func DeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
|
||||
// TestDeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing
|
||||
func TestDeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
|
||||
return deleteOldAlertsHistory(app, countToKeep, countBeforeDeletion)
|
||||
}
|
||||
|
||||
// TwoDecimals exposes twoDecimals for testing
|
||||
func TwoDecimals(value float64) float64 {
|
||||
// TestTwoDecimals exposes twoDecimals for testing
|
||||
func TestTwoDecimals(value float64) float64 {
|
||||
return twoDecimals(value)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
}
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
@@ -38,4 +35,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
internal/site/package-lock.json
generated
186
internal/site/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "beszel",
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.7",
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
"@henrygd/semaphore": "^0.0.2",
|
||||
@@ -46,7 +46,6 @@
|
||||
"valibot": "^0.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.3",
|
||||
"@lingui/cli": "^5.4.1",
|
||||
"@lingui/swc-plugin": "^5.6.1",
|
||||
"@lingui/vite-plugin": "^5.4.1",
|
||||
@@ -331,169 +330,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/biome": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.3.tgz",
|
||||
"integrity": "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"bin": {
|
||||
"biome": "bin/biome"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/biome"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@biomejs/cli-darwin-arm64": "2.2.3",
|
||||
"@biomejs/cli-darwin-x64": "2.2.3",
|
||||
"@biomejs/cli-linux-arm64": "2.2.3",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.2.3",
|
||||
"@biomejs/cli-linux-x64": "2.2.3",
|
||||
"@biomejs/cli-linux-x64-musl": "2.2.3",
|
||||
"@biomejs/cli-win32-arm64": "2.2.3",
|
||||
"@biomejs/cli-win32-x64": "2.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.3.tgz",
|
||||
"integrity": "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-x64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.3.tgz",
|
||||
"integrity": "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.3.tgz",
|
||||
"integrity": "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.3.tgz",
|
||||
"integrity": "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.3.tgz",
|
||||
"integrity": "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.3.tgz",
|
||||
"integrity": "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-arm64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.3.tgz",
|
||||
"integrity": "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-x64": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.3.tgz",
|
||||
"integrity": "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
||||
@@ -5927,14 +5763,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -6121,9 +5957,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
|
||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
||||
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6132,7 +5968,7 @@
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -76,4 +76,4 @@
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-arm64": "^0.21.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -14,30 +10,34 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { $publicKey } from "@/lib/stores"
|
||||
import { cn, generateToken, tokenMap, useBrowserStorage } from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import { pb, isReadOnlyUser } from "@/lib/api"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { $router, basePath, Link, navigate } from "./router"
|
||||
import { SystemRecord } from "@/types"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||
import { InputCopy } from "./ui/input-copy"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import {
|
||||
copyDockerCompose,
|
||||
copyDockerRun,
|
||||
copyLinuxCommand,
|
||||
copyWindowsCommand,
|
||||
type DropdownItem,
|
||||
DropdownItem,
|
||||
InstallDropdown,
|
||||
} from "./install-dropdowns"
|
||||
import { $router, basePath, Link, navigate } from "./router"
|
||||
import { DropdownMenu, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
||||
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||
import { InputCopy } from "./ui/input-copy"
|
||||
|
||||
export function AddSystemButton({ className }: { className?: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const opened = useRef(false)
|
||||
let opened = useRef(false)
|
||||
if (open) {
|
||||
opened.current = true
|
||||
}
|
||||
@@ -253,12 +253,6 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),
|
||||
icons: [WindowsIcon],
|
||||
},
|
||||
{
|
||||
text: t({ message: "FreeBSD command", context: "Button to copy install command" }),
|
||||
onClick: async () =>
|
||||
copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),
|
||||
icons: [FreeBsdIcon],
|
||||
},
|
||||
{
|
||||
text: t`Manual setup instructions`,
|
||||
url: "https://beszel.dev/guide/agent-installation#binary",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { AlertsHistoryRecord } from "@/types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { cn, formatDuration, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { AlertsHistoryRecord } from "@/types"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
|
||||
{
|
||||
@@ -38,7 +38,7 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
|
||||
</Button>
|
||||
),
|
||||
cell: ({ getValue, row }) => {
|
||||
const name = getValue() as string
|
||||
let name = getValue() as string
|
||||
const info = alertInfo[row.original.name]
|
||||
const Icon = info?.icon
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { BellIcon } from "lucide-react"
|
||||
import { memo, useMemo, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $alerts } from "@/lib/stores"
|
||||
import { BellIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { SystemRecord } from "@/types"
|
||||
import { AlertDialogContent } from "./alerts-sheet"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
|
||||
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Plural, Trans } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { GlobeIcon, ServerIcon } from "lucide-react"
|
||||
import { lazy, memo, Suspense, useMemo, useState } from "react"
|
||||
import { $router, Link } from "@/components/router"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { pb } from "@/lib/api"
|
||||
import { Trans, Plural } from "@lingui/react/macro"
|
||||
import { $alerts, $systems } from "@/lib/stores"
|
||||
import { cn, debounce } from "@/lib/utils"
|
||||
import type { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
||||
import { lazy, memo, Suspense, useMemo, useState } from "react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
import { ServerIcon, GlobeIcon } from "lucide-react"
|
||||
import { $router, Link } from "@/components/router"
|
||||
import { DialogHeader } from "@/components/ui/dialog"
|
||||
import { pb } from "@/lib/api"
|
||||
|
||||
const Slider = lazy(() => import("@/components/ui/slider"))
|
||||
|
||||
@@ -171,7 +172,7 @@ export function AlertContent({
|
||||
|
||||
const [checked, setChecked] = useState(global ? false : !!alert)
|
||||
const [min, setMin] = useState(alert?.min || 10)
|
||||
const [value, setValue] = useState(alert?.value || (singleDescription ? 0 : (alertData.start ?? 80)))
|
||||
const [value, setValue] = useState(alert?.value || (singleDescription ? 0 : alertData.start ?? 80))
|
||||
|
||||
const Icon = alertData.icon
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useMemo } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
import { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useMemo } from "react"
|
||||
|
||||
export type DataPoint = {
|
||||
label: string
|
||||
@@ -27,8 +20,6 @@ export default function AreaChartDefault({
|
||||
contentFormatter,
|
||||
dataPoints,
|
||||
domain,
|
||||
legend,
|
||||
itemSorter,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
@@ -38,13 +29,10 @@ export default function AreaChartDefault({
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: [number, number]
|
||||
legend?: boolean
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
|
||||
return useMemo(() => {
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
@@ -75,8 +63,6 @@ export default function AreaChartDefault({
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
itemSorter={itemSorter}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
@@ -84,14 +70,11 @@ export default function AreaChartDefault({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataPoints?.map((dataPoint) => {
|
||||
let { color } = dataPoint
|
||||
if (typeof color === "number") {
|
||||
color = `var(--chart-${color})`
|
||||
}
|
||||
{dataPoints?.map((dataPoint, i) => {
|
||||
const color = `var(--chart-${dataPoint.color})`
|
||||
return (
|
||||
<Area
|
||||
key={dataPoint.label}
|
||||
key={i}
|
||||
dataKey={dataPoint.dataKey}
|
||||
name={dataPoint.label}
|
||||
type="monotoneX"
|
||||
@@ -102,7 +85,7 @@ export default function AreaChartDefault({
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{legend && <ChartLegend content={<ChartLegendContent />} />}
|
||||
{/* <ChartLegend content={<ChartLegendContent />} /> */}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { HistoryIcon } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { $chartTime } from "@/lib/stores"
|
||||
import { chartTimeData, cn } from "@/lib/utils"
|
||||
import type { ChartTimes } from "@/types"
|
||||
import { ChartTimes } from "@/types"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { HistoryIcon } from "lucide-react"
|
||||
|
||||
export default function ChartTimeSelect({ className }: { className?: string }) {
|
||||
const chartTime = useStore($chartTime)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// import Spinner from '../spinner'
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { memo, useMemo } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { ChartType, Unit } from "@/lib/enums"
|
||||
import { memo, useMemo } from "react"
|
||||
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { ChartType, Unit } from "@/lib/enums"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function ContainerChart({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { memo } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Unit } from "@/lib/enums"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useMemo } from "react"
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
|
||||
@@ -26,10 +27,10 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
||||
colors: Record<string, string>
|
||||
}
|
||||
const powerSums = {} as Record<string, number>
|
||||
for (const data of chartData.systemStats) {
|
||||
const newData = { created: data.created } as Record<string, number | string>
|
||||
for (let data of chartData.systemStats) {
|
||||
let newData = { created: data.created } as Record<string, number | string>
|
||||
|
||||
for (const gpu of Object.values(data.stats?.g ?? {})) {
|
||||
for (let gpu of Object.values(data.stats?.g ?? {})) {
|
||||
if (gpu.p) {
|
||||
const name = gpu.n
|
||||
newData[name] = gpu.p
|
||||
@@ -39,7 +40,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
||||
newChartData.data.push(newData)
|
||||
}
|
||||
const keys = Object.keys(powerSums).sort((a, b) => powerSums[b] - powerSums[a])
|
||||
for (const key of keys) {
|
||||
for (let key of keys) {
|
||||
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
||||
}
|
||||
return newChartData
|
||||
@@ -66,7 +67,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => {
|
||||
const val = toFixedFloat(value, 2)
|
||||
return updateYAxisWidth(`${val}W`)
|
||||
return updateYAxisWidth(val + "W")
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -75,12 +76,12 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => `${decimalString(item.value)}W`}
|
||||
contentFormatter={(item) => decimalString(item.value) + "W"}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import type { ChartConfig } from "@/components/ui/chart"
|
||||
import type { ChartData, SystemStats, SystemStatsRecord } from "@/types"
|
||||
import { ChartConfig } from "@/components/ui/chart"
|
||||
import { ChartData } from "@/types"
|
||||
|
||||
/** Chart configurations for CPU, memory, and network usage charts */
|
||||
export interface ContainerChartConfigs {
|
||||
@@ -105,21 +105,3 @@ export function useYAxisWidth() {
|
||||
}
|
||||
return { yAxisWidth, updateYAxisWidth }
|
||||
}
|
||||
|
||||
// Assures consistent colors for network interfaces
|
||||
export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
|
||||
const keys = Object.keys(interfaces ?? {})
|
||||
const sortedKeys = keys.sort((a, b) => (interfaces?.[b]?.[3] ?? 0) - (interfaces?.[a]?.[3] ?? 0))
|
||||
return {
|
||||
length: sortedKeys.length,
|
||||
data: (index = 3) => {
|
||||
return sortedKeys.map((key) => ({
|
||||
label: key,
|
||||
dataKey: (stats: SystemStatsRecord) => stats.stats?.ni?.[key]?.[index],
|
||||
color: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`,
|
||||
|
||||
opacity: 0.3,
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useMemo } from "react"
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export type DataPoint = {
|
||||
label: string
|
||||
dataKey: (data: SystemStatsRecord) => number | undefined
|
||||
color: number | string
|
||||
}
|
||||
|
||||
export default function LineChartDefault({
|
||||
chartData,
|
||||
max,
|
||||
maxToggled,
|
||||
tickFormatter,
|
||||
contentFormatter,
|
||||
dataPoints,
|
||||
domain,
|
||||
legend,
|
||||
itemSorter,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: [number, number]
|
||||
legend?: boolean
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
|
||||
return useMemo(() => {
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
}
|
||||
// if (logRender) {
|
||||
// console.log("Rendered at", new Date())
|
||||
// }
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<LineChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={domain ?? [0, max ?? "auto"]}
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
itemSorter={itemSorter}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={contentFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataPoints?.map((dataPoint) => {
|
||||
let { color } = dataPoint
|
||||
if (typeof color === "number") {
|
||||
color = `var(--chart-${color})`
|
||||
}
|
||||
return (
|
||||
<Line
|
||||
key={dataPoint.label}
|
||||
dataKey={dataPoint.dataKey}
|
||||
name={dataPoint.label}
|
||||
type="monotoneX"
|
||||
dot={false}
|
||||
strokeWidth={1.5}
|
||||
stroke={color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{legend && <ChartLegend content={<ChartLegendContent />} />}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { memo } from "react"
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
@@ -9,8 +8,10 @@ import {
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData, SystemStats } from "@/types"
|
||||
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||
import { ChartData, SystemStats } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
||||
@@ -59,7 +60,7 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
// itemSorter={(a, b) => b.value - a.value}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { memo } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { memo } from "react"
|
||||
import { ChartData } from "@/types"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Unit } from "@/lib/enums"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
|
||||
@@ -53,7 +53,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => a.order - b.order}
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={({ value }) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { memo } from "react"
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { $userSettings } from "@/lib/stores"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { memo, useMemo } from "react"
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
@@ -9,9 +8,11 @@ import {
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { cn, formatShortDate, toFixedFloat, chartMargin, formatTemperature, decimalString } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { $temperatureFilter, $userSettings } from "@/lib/stores"
|
||||
import { chartMargin, cn, decimalString, formatShortDate, formatTemperature, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||
@@ -30,18 +31,18 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
colors: Record<string, string>
|
||||
}
|
||||
const tempSums = {} as Record<string, number>
|
||||
for (const data of chartData.systemStats) {
|
||||
const newData = { created: data.created } as Record<string, number | string>
|
||||
const keys = Object.keys(data.stats?.t ?? {})
|
||||
for (let data of chartData.systemStats) {
|
||||
let newData = { created: data.created } as Record<string, number | string>
|
||||
let keys = Object.keys(data.stats?.t ?? {})
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
let key = keys[i]
|
||||
newData[key] = data.stats.t![key]
|
||||
tempSums[key] = (tempSums[key] ?? 0) + newData[key]
|
||||
}
|
||||
newChartData.data.push(newData)
|
||||
}
|
||||
const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
|
||||
for (const key of keys) {
|
||||
for (let key of keys) {
|
||||
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
||||
}
|
||||
return newChartData
|
||||
@@ -77,7 +78,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
@@ -92,7 +93,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
/>
|
||||
{colors.map((key) => {
|
||||
const filtered = filter && !key.toLowerCase().includes(filter.toLowerCase())
|
||||
const strokeOpacity = filtered ? 0.1 : 1
|
||||
let strokeOpacity = filtered ? 0.1 : 1
|
||||
return (
|
||||
<Line
|
||||
key={key}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { DialogDescription } from "@radix-ui/react-dialog"
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
BookIcon,
|
||||
@@ -14,7 +10,7 @@ import {
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo } from "react"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -25,10 +21,15 @@ import {
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command"
|
||||
import { isAdmin } from "@/lib/api"
|
||||
import { memo, useEffect, useMemo } from "react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { getHostDisplayValue, listen } from "@/lib/utils"
|
||||
import { $router, basePath, navigate, prependBasePath } from "./router"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { DialogDescription } from "@radix-ui/react-dialog"
|
||||
import { isAdmin } from "@/lib/api"
|
||||
|
||||
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { $copyContent } from "@/lib/stores"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { $copyContent } from "@/lib/stores"
|
||||
|
||||
export default function CopyToClipboard({ content }: { content: string }) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { i18n } from "@lingui/core"
|
||||
import { memo } from "react"
|
||||
import { copyToClipboard, getHubURL } from "@/lib/utils"
|
||||
import { DropdownMenuContent, DropdownMenuItem } from "./ui/dropdown-menu"
|
||||
import { copyToClipboard, getHubURL } from "@/lib/utils"
|
||||
import { i18n } from "@lingui/core"
|
||||
|
||||
// const isbeta = beszel.hub_version.includes("beta")
|
||||
// const imagetag = isbeta ? ":edge" : ""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { LanguagesIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import languages from "@/lib/languages"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
|
||||
export function LangToggle() {
|
||||
const { i18n } = useLingui()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { KeyIcon, LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
|
||||
import type { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import * as v from "valibot"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { pb } from "@/lib/api"
|
||||
import { KeyIcon, LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
|
||||
import { $authenticated } from "@/lib/stores"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { $router, Link, prependBasePath } from "../router"
|
||||
import * as v from "valibot"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
|
||||
import { $router, Link, prependBasePath } from "../router"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { pb } from "@/lib/api"
|
||||
import { OtpInputForm } from "./otp-forms"
|
||||
|
||||
const honeypot = v.literal("")
|
||||
@@ -83,9 +83,9 @@ export function UserAuthForm({
|
||||
const result = v.safeParse(Schema, data)
|
||||
if (!result.success) {
|
||||
console.log(result)
|
||||
const errors = {}
|
||||
let errors = {}
|
||||
for (const issue of result.issues) {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
errors[issue.path[0].key] = issue.message
|
||||
}
|
||||
setErrors(errors)
|
||||
@@ -96,7 +96,7 @@ export function UserAuthForm({
|
||||
if (isFirstRun) {
|
||||
// check that passwords match
|
||||
if (password !== passwordConfirm) {
|
||||
const msg = "Passwords do not match"
|
||||
let msg = "Passwords do not match"
|
||||
setErrors({ passwordConfirm: msg })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { pb } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "../ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"
|
||||
import { Input } from "../ui/input"
|
||||
import { Label } from "../ui/label"
|
||||
import { useCallback, useState } from "react"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { buttonVariants } from "../ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogHeader } from "../ui/dialog"
|
||||
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
|
||||
import { pb } from "@/lib/api"
|
||||
|
||||
const showLoginFaliedToast = () => {
|
||||
toast({
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import type { AuthMethodsList } from "pocketbase"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { UserAuthForm } from "@/components/login/auth-form"
|
||||
import { pb } from "@/lib/api"
|
||||
import { Logo } from "../logo"
|
||||
import { ModeToggle } from "../mode-toggle"
|
||||
import { $router } from "../router"
|
||||
import { useTheme } from "../theme-provider"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import ForgotPassword from "./forgot-pass-form"
|
||||
import { $router } from "../router"
|
||||
import { AuthMethodsList } from "pocketbase"
|
||||
import { useTheme } from "../theme-provider"
|
||||
import { pb } from "@/lib/api"
|
||||
import { ModeToggle } from "../mode-toggle"
|
||||
import { OtpRequestForm } from "./otp-forms"
|
||||
|
||||
export default function () {
|
||||
@@ -53,7 +53,7 @@ export default function () {
|
||||
<div className="min-h-svh grid items-center py-12">
|
||||
<div
|
||||
className="grid gap-5 w-full px-4 mx-auto"
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
style={{ maxWidth: "21.5em", "--border": theme == "light" ? "hsl(30, 8%, 70%)" : "hsl(220, 3%, 25%)" }}
|
||||
>
|
||||
<div className="absolute top-3 right-3">
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/otp"
|
||||
import { pb } from "@/lib/api"
|
||||
import { $authenticated } from "@/lib/stores"
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/otp"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { showLoginFaliedToast } from "./auth-form"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { $router } from "../router"
|
||||
import { MailIcon, LoaderCircle, SendHorizonalIcon } from "lucide-react"
|
||||
import { Label } from "../ui/label"
|
||||
import { buttonVariants } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
import { Label } from "../ui/label"
|
||||
import { showLoginFaliedToast } from "./auth-form"
|
||||
import { $router } from "../router"
|
||||
|
||||
export function OtpInputForm({ otpId, mfaId }: { otpId: string; mfaId: string }) {
|
||||
const [value, setValue] = useState("")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { MoonStarIcon, SunIcon } from "lucide-react"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { useState, lazy, Suspense } from "react"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import {
|
||||
DatabaseBackupIcon,
|
||||
LogOutIcon,
|
||||
@@ -10,24 +11,23 @@ import {
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react"
|
||||
import { lazy, Suspense, useState } from "react"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||
import { LangToggle } from "./lang-toggle"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { Logo } from "./logo"
|
||||
import { cn, runOnce } from "@/lib/utils"
|
||||
import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api"
|
||||
import { cn, runOnce } from "@/lib/utils"
|
||||
import { AddSystemButton } from "./add-system"
|
||||
import { LangToggle } from "./lang-toggle"
|
||||
import { Logo } from "./logo"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const CommandPalette = lazy(() => import("./command-palette"))
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const prependBasePath = (path: string) => (basePath + path).replaceAll("/
|
||||
|
||||
// prepend base path to routes
|
||||
for (const route in routes) {
|
||||
// @ts-expect-error need as const above to get nanostores to parse types properly
|
||||
// @ts-ignore need as const above to get nanostores to parse types properly
|
||||
routes[route] = prependBasePath(routes[route])
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import {
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
FingerprintIcon,
|
||||
KeyIcon,
|
||||
MoreHorizontalIcon,
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -151,7 +150,6 @@ const SectionUniversalToken = memo(() => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only on mount
|
||||
useEffect(() => {
|
||||
updateToken()
|
||||
}, [])
|
||||
@@ -223,16 +221,6 @@ const ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; c
|
||||
onClick: () => copyWindowsCommand(port, publicKey, token),
|
||||
icons: [WindowsIcon],
|
||||
},
|
||||
{
|
||||
text: t({ message: "FreeBSD command", context: "Button to copy install command" }),
|
||||
onClick: () => copyLinuxCommand(port, publicKey, token),
|
||||
icons: [FreeBsdIcon],
|
||||
},
|
||||
{
|
||||
text: t`Manual setup instructions`,
|
||||
url: "https://beszel.dev/guide/agent-installation#binary",
|
||||
icons: [ExternalLinkIcon],
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -303,8 +291,8 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody className="whitespace-pre">
|
||||
{fingerprints.map((fingerprint) => (
|
||||
<TableRow key={fingerprint.id}>
|
||||
{fingerprints.map((fingerprint, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
|
||||
{fingerprint.expand.system.name}
|
||||
</TableCell>
|
||||
@@ -329,10 +317,10 @@ async function updateFingerprint(fingerprint: FingerprintRecord, rotateToken = f
|
||||
fingerprint: "",
|
||||
token: rotateToken ? generateToken() : fingerprint.token,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: (error as Error).message,
|
||||
description: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ import { Input } from "../ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import NetworkSheet from "./system/network-sheet"
|
||||
|
||||
type ChartTimeData = {
|
||||
time: number
|
||||
@@ -565,13 +564,13 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
dataPoints={[
|
||||
{
|
||||
label: t({ message: "Write", comment: "Disk write" }),
|
||||
dataKey: ({ stats }: SystemStatsRecord) => (showMax ? stats?.dwm : stats?.dw),
|
||||
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
|
||||
color: 3,
|
||||
opacity: 0.3,
|
||||
},
|
||||
{
|
||||
label: t({ message: "Read", comment: "Disk read" }),
|
||||
dataKey: ({ stats }: SystemStatsRecord) => (showMax ? stats?.drm : stats?.dr),
|
||||
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
|
||||
color: 1,
|
||||
opacity: 0.3,
|
||||
},
|
||||
@@ -591,12 +590,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Bandwidth`}
|
||||
cornerEl={
|
||||
<div className="flex gap-2">
|
||||
{maxValSelect}
|
||||
<NetworkSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />
|
||||
</div>
|
||||
}
|
||||
cornerEl={maxValSelect}
|
||||
description={t`Network traffic of public interfaces`}
|
||||
>
|
||||
<AreaChartDefault
|
||||
@@ -606,7 +600,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
{
|
||||
label: t`Sent`,
|
||||
// use bytes if available, otherwise multiply old MB (can remove in future)
|
||||
dataKey(data: SystemStatsRecord) {
|
||||
dataKey(data) {
|
||||
if (showMax) {
|
||||
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
|
||||
}
|
||||
@@ -617,7 +611,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
},
|
||||
{
|
||||
label: t`Received`,
|
||||
dataKey(data: SystemStatsRecord) {
|
||||
dataKey(data) {
|
||||
if (showMax) {
|
||||
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
|
||||
}
|
||||
@@ -626,9 +620,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
color: 2,
|
||||
opacity: 0.2,
|
||||
},
|
||||
]
|
||||
// try to place the lesser number in front for better visibility
|
||||
.sort(() => (systemStats.at(-1)?.stats.b?.[1] ?? 0) - (systemStats.at(-1)?.stats.b?.[0] ?? 0))}
|
||||
]}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
@@ -682,7 +674,6 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
grid={grid}
|
||||
title={t`Load Average`}
|
||||
description={t`System load averages over time`}
|
||||
legend={true}
|
||||
>
|
||||
<LoadAverageChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
@@ -696,7 +687,6 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
title={t`Temperature`}
|
||||
description={t`Temperatures of system sensors`}
|
||||
cornerEl={<FilterBar store={$temperatureFilter} />}
|
||||
legend={Object.keys(systemStats.at(-1)?.stats.t ?? {}).length < 12}
|
||||
>
|
||||
<TemperatureChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
@@ -889,7 +879,7 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input placeholder={t`Filter...`} className="ps-4 pe-8 w-full sm:w-44" onChange={handleChange} ref={inputRef} />
|
||||
<Input placeholder={t`Filter...`} className="ps-4 pe-8" onChange={handleChange} ref={inputRef} />
|
||||
{containerFilter && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -915,7 +905,7 @@ const SelectAvgMax = memo(({ max }: { max: boolean }) => {
|
||||
const Icon = max ? ChartMax : ChartAverage
|
||||
return (
|
||||
<Select value={max ? "max" : "avg"} onValueChange={(e) => $maxValues.set(e === "max")}>
|
||||
<SelectTrigger className="relative ps-10 pe-5 w-full sm:w-44">
|
||||
<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 />
|
||||
</SelectTrigger>
|
||||
@@ -931,15 +921,13 @@ const SelectAvgMax = memo(({ max }: { max: boolean }) => {
|
||||
)
|
||||
})
|
||||
|
||||
export function ChartCard({
|
||||
function ChartCard({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
grid,
|
||||
empty,
|
||||
cornerEl,
|
||||
legend,
|
||||
className,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
@@ -947,24 +935,17 @@ export function ChartCard({
|
||||
grid?: boolean
|
||||
empty?: boolean
|
||||
cornerEl?: JSX.Element | null
|
||||
legend?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver()
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full min-h-full", { "col-span-full": !grid }, className)}
|
||||
ref={ref}
|
||||
>
|
||||
<Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
|
||||
<CardHeader className="pb-5 pt-4 gap-1 relative max-sm:py-3 max-sm:px-4">
|
||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
{cornerEl && (
|
||||
<div className="relative py-1 grid sm:justify-end sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>
|
||||
)}
|
||||
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>}
|
||||
</CardHeader>
|
||||
<div className={cn("ps-0 w-[calc(100%-1.5em)] relative group", legend ? "h-54 md:h-56" : "h-48 md:h-52")}>
|
||||
<div className="ps-0 w-[calc(100%-1.5em)] h-48 md:h-52 relative group">
|
||||
{
|
||||
<Spinner
|
||||
msg={empty ? t`Waiting for enough records to display` : undefined}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { MoreHorizontalIcon } from "lucide-react"
|
||||
import { memo, useRef, useState } from "react"
|
||||
import AreaChartDefault from "@/components/charts/area-chart"
|
||||
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||
import { useNetworkInterfaces } from "@/components/charts/hooks"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { $userSettings } from "@/lib/stores"
|
||||
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import { ChartCard } from "../system"
|
||||
|
||||
export default memo(function NetworkSheet({
|
||||
chartData,
|
||||
dataEmpty,
|
||||
grid,
|
||||
maxValues,
|
||||
}: {
|
||||
chartData: ChartData
|
||||
dataEmpty: boolean
|
||||
grid: boolean
|
||||
maxValues: boolean
|
||||
}) {
|
||||
const [netInterfacesOpen, setNetInterfacesOpen] = useState(false)
|
||||
const userSettings = useStore($userSettings)
|
||||
const netInterfaces = useNetworkInterfaces(chartData.systemStats.at(-1)?.stats?.ni ?? {})
|
||||
const showNetLegend = netInterfaces.length > 0
|
||||
const hasOpened = useRef(false)
|
||||
|
||||
if (netInterfacesOpen && !hasOpened.current) {
|
||||
hasOpened.current = true
|
||||
}
|
||||
|
||||
if (!netInterfaces.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
{hasOpened.current && (
|
||||
<SheetContent className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
|
||||
<ChartTimeSelect className="w-[calc(100%-1.5em)]" />
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Download`}
|
||||
description={t`Network traffic of public interfaces`}
|
||||
legend={showNetLegend}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={maxValues}
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
dataPoints={netInterfaces.data(1)}
|
||||
legend={showNetLegend}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)
|
||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Upload`}
|
||||
description={t`Network traffic of public interfaces`}
|
||||
legend={showNetLegend}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={maxValues}
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
legend={showNetLegend}
|
||||
dataPoints={netInterfaces.data(0)}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)
|
||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Cumulative Download`}
|
||||
description={t`Total data received for each interface`}
|
||||
legend={showNetLegend}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
legend={showNetLegend}
|
||||
dataPoints={netInterfaces.data(3)}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, userSettings.unitNet, false)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)
|
||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Cumulative Upload`}
|
||||
description={t`Total data sent for each interface`}
|
||||
legend={showNetLegend}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
legend={showNetLegend}
|
||||
dataPoints={netInterfaces.data(2)}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, userSettings.unitNet, false)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)
|
||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
</SheetContent>
|
||||
)}
|
||||
</Sheet>
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoaderCircleIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LoaderCircleIcon } from "lucide-react"
|
||||
|
||||
export default function ({ msg, className }: { msg?: string; className?: string }) {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
|
||||
import type { ClassValue } from "clsx"
|
||||
import { SystemRecord } from "@/types"
|
||||
import { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
|
||||
import { ClassValue } from "clsx"
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
CopyIcon,
|
||||
@@ -18,10 +15,7 @@ import {
|
||||
Trash2Icon,
|
||||
WifiIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useMemo, useRef, useState } from "react"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { MeterState, SystemStatus } from "@/lib/enums"
|
||||
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
||||
import { Button } from "../ui/button"
|
||||
import {
|
||||
cn,
|
||||
copyToClipboard,
|
||||
@@ -31,12 +25,24 @@ import {
|
||||
getMeterState,
|
||||
parseSemVer,
|
||||
} from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import { SystemDialog } from "../add-system"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { $router, Link } from "../router"
|
||||
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useMemo, useRef, useState } from "react"
|
||||
import { memo } from "react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { Dialog } from "../ui/dialog"
|
||||
import { SystemDialog } from "../add-system"
|
||||
import { AlertDialog } from "../ui/alert-dialog"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
@@ -45,16 +51,12 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "../ui/alert-dialog"
|
||||
import { Button, buttonVariants } from "../ui/button"
|
||||
import { Dialog } from "../ui/dialog"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu"
|
||||
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { buttonVariants } from "../ui/button"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { MeterState, SystemStatus } from "@/lib/enums"
|
||||
import { $router, Link } from "../router"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
|
||||
const STATUS_COLORS = {
|
||||
[SystemStatus.Up]: "bg-green-500",
|
||||
@@ -288,7 +290,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
name: () => t({ message: "Actions", comment: "Table column" }),
|
||||
size: 50,
|
||||
cell: ({ row }) => (
|
||||
@@ -303,7 +305,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
|
||||
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
|
||||
const { column } = context
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
|
||||
return (
|
||||
<Button
|
||||
@@ -351,7 +353,7 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
|
||||
export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const editOpened = useRef(false)
|
||||
let editOpened = useRef(false)
|
||||
const { t } = useLingui()
|
||||
const { id, status, host, name } = system
|
||||
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import {
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type Table as TableType,
|
||||
flexRender,
|
||||
VisibilityState,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
Row,
|
||||
Table as TableType,
|
||||
} from "@tanstack/react-table"
|
||||
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpDownIcon,
|
||||
ArrowUpIcon,
|
||||
EyeIcon,
|
||||
FilterIcon,
|
||||
LayoutGridIcon,
|
||||
LayoutListIcon,
|
||||
Settings2Icon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -38,16 +24,30 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { $downSystems, $pausedSystems, $systems, $upSystems } from "@/lib/stores"
|
||||
import { SystemRecord } from "@/types"
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
LayoutGridIcon,
|
||||
LayoutListIcon,
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
Settings2Icon,
|
||||
EyeIcon,
|
||||
FilterIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { $router, Link } from "../router"
|
||||
import { useLingui, Trans } from "@lingui/react/macro"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
type StatusFilter = "all" | SystemRecord["status"]
|
||||
@@ -309,63 +309,69 @@ export default function SystemsTable() {
|
||||
)
|
||||
}
|
||||
|
||||
const AllSystemsTable = memo(
|
||||
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
|
||||
// The virtualizer will need a reference to the scrollable container element
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const AllSystemsTable = memo(function ({
|
||||
table,
|
||||
rows,
|
||||
colLength,
|
||||
}: {
|
||||
table: TableType<SystemRecord>
|
||||
rows: Row<SystemRecord>[]
|
||||
colLength: number
|
||||
}) {
|
||||
// The virtualizer will need a reference to the scrollable container element
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
||||
count: rows.length,
|
||||
estimateSize: () => (rows.length > 10 ? 56 : 60),
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 5,
|
||||
})
|
||||
const virtualRows = virtualizer.getVirtualItems()
|
||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
||||
count: rows.length,
|
||||
estimateSize: () => (rows.length > 10 ? 56 : 60),
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 5,
|
||||
})
|
||||
const virtualRows = virtualizer.getVirtualItems()
|
||||
|
||||
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
|
||||
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
|
||||
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
|
||||
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
|
||||
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
|
||||
(!rows.length || rows.length > 2) && "min-h-50"
|
||||
)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{/* add header height to table size */}
|
||||
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
|
||||
<table className="text-sm w-full h-full">
|
||||
<SystemsTableHead table={table} colLength={colLength} />
|
||||
<TableBody onMouseEnter={preloadSystemDetail}>
|
||||
{rows.length ? (
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<SystemRecord>
|
||||
return (
|
||||
<SystemTableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
virtualRow={virtualRow}
|
||||
length={rows.length}
|
||||
colLength={colLength}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
|
||||
<Trans>No systems found.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
|
||||
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
|
||||
(!rows.length || rows.length > 2) && "min-h-50"
|
||||
)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{/* add header height to table size */}
|
||||
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
|
||||
<table className="text-sm w-full h-full">
|
||||
<SystemsTableHead table={table} colLength={colLength} />
|
||||
<TableBody onMouseEnter={preloadSystemDetail}>
|
||||
{rows.length ? (
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<SystemRecord>
|
||||
return (
|
||||
<SystemTableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
virtualRow={virtualRow}
|
||||
length={rows.length}
|
||||
colLength={colLength}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
|
||||
<Trans>No systems found.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
|
||||
const { i18n } = useLingui()
|
||||
@@ -389,44 +395,42 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
|
||||
}, [i18n.locale, colLength])
|
||||
}
|
||||
|
||||
const SystemTableRow = memo(
|
||||
({
|
||||
row,
|
||||
virtualRow,
|
||||
colLength,
|
||||
}: {
|
||||
row: Row<SystemRecord>
|
||||
virtualRow: VirtualItem
|
||||
length: number
|
||||
colLength: number
|
||||
}) => {
|
||||
const system = row.original
|
||||
const { t } = useLingui()
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<TableRow
|
||||
// data-state={row.getIsSelected() && "selected"}
|
||||
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
|
||||
"opacity-50": system.status === SystemStatus.Paused,
|
||||
})}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
height: virtualRow.size,
|
||||
}}
|
||||
className="py-0"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}, [system, system.status, colLength, t])
|
||||
}
|
||||
)
|
||||
const SystemTableRow = memo(function ({
|
||||
row,
|
||||
virtualRow,
|
||||
colLength,
|
||||
}: {
|
||||
row: Row<SystemRecord>
|
||||
virtualRow: VirtualItem
|
||||
length: number
|
||||
colLength: number
|
||||
}) {
|
||||
const system = row.original
|
||||
const { t } = useLingui()
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<TableRow
|
||||
// data-state={row.getIsSelected() && "selected"}
|
||||
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
|
||||
"opacity-50": system.status === SystemStatus.Paused,
|
||||
})}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
height: virtualRow.size,
|
||||
}}
|
||||
className="py-0"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}, [system, system.status, colLength, t])
|
||||
})
|
||||
|
||||
const SystemCard = memo(
|
||||
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
|
||||
@@ -467,7 +471,7 @@ const SystemCard = memo(
|
||||
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
|
||||
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
|
||||
if (!cell) return null
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/** biome-ignore-all lint/suspicious/noAssignInExpressions: it's fine :) */
|
||||
import type { PreinitializedMapStore } from "nanostores"
|
||||
import { pb, verifyAuth } from "@/lib/api"
|
||||
import {
|
||||
@@ -17,10 +16,9 @@ const COLLECTION = pb.collection<SystemRecord>("systems")
|
||||
const FIELDS_DEFAULT = "id,name,host,port,info,status"
|
||||
|
||||
/** Maximum system name length for display purposes */
|
||||
const MAX_SYSTEM_NAME_LENGTH = 22
|
||||
const MAX_SYSTEM_NAME_LENGTH = 20
|
||||
|
||||
let initialized = false
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: typescript rocks
|
||||
let unsub: (() => void) | undefined | void
|
||||
|
||||
/** Initialize the systems manager and set up listeners */
|
||||
@@ -106,37 +104,20 @@ async function fetchSystems(): Promise<SystemRecord[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Makes sure the system has valid info object and throws if not */
|
||||
function validateSystemInfo(system: SystemRecord) {
|
||||
if (!("cpu" in system.info)) {
|
||||
throw new Error(`${system.name} has no CPU info`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Add system to both name and ID stores */
|
||||
export function add(system: SystemRecord) {
|
||||
try {
|
||||
validateSystemInfo(system)
|
||||
$allSystemsByName.setKey(system.name, system)
|
||||
$allSystemsById.setKey(system.id, system)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
$allSystemsByName.setKey(system.name, system)
|
||||
$allSystemsById.setKey(system.id, system)
|
||||
}
|
||||
|
||||
/** Update system in stores */
|
||||
export function update(system: SystemRecord) {
|
||||
try {
|
||||
validateSystemInfo(system)
|
||||
// if name changed, make sure old name is removed from the name store
|
||||
const oldName = $allSystemsById.get()[system.id]?.name
|
||||
if (oldName !== system.name) {
|
||||
$allSystemsByName.setKey(oldName, undefined as unknown as SystemRecord)
|
||||
}
|
||||
add(system)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// if name changed, make sure old name is removed from the name store
|
||||
const oldName = $allSystemsById.get()[system.id]?.name
|
||||
if (oldName !== system.name) {
|
||||
$allSystemsByName.setKey(oldName, undefined as any)
|
||||
}
|
||||
add(system)
|
||||
}
|
||||
|
||||
/** Remove system from stores */
|
||||
@@ -151,7 +132,7 @@ export function remove(system: SystemRecord) {
|
||||
/** Remove system from specific store */
|
||||
function removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {
|
||||
const key = store === $allSystemsByName ? system.name : system.id
|
||||
store.setKey(key, undefined as unknown as SystemRecord)
|
||||
store.setKey(key, undefined as any)
|
||||
}
|
||||
|
||||
/** Action functions for subscription */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { timeDay, timeHour } from "d3-time"
|
||||
import { listenKeys } from "nanostores"
|
||||
import { useEffect, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
@@ -9,6 +8,7 @@ import { toast } from "@/components/ui/use-toast"
|
||||
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
|
||||
import { HourFormat, MeterState, Unit } from "./enums"
|
||||
import { $copyContent, $userSettings } from "./stores"
|
||||
import { listenKeys } from "nanostores"
|
||||
|
||||
export const FAVICON_DEFAULT = "favicon.svg"
|
||||
export const FAVICON_GREEN = "favicon-green.svg"
|
||||
|
||||
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
1296
internal/site/src/locales/el/el.po
Normal file
1296
internal/site/src/locales/el/el.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -358,14 +358,6 @@ msgstr "Created"
|
||||
msgid "Critical (%)"
|
||||
msgstr "Critical (%)"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Download"
|
||||
msgstr "Cumulative Download"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Upload"
|
||||
msgstr "Cumulative Upload"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Current state"
|
||||
@@ -444,10 +436,6 @@ msgstr "Down"
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr "Down ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Duration"
|
||||
msgstr "Duration"
|
||||
@@ -459,7 +447,6 @@ msgstr "Edit"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
@@ -480,10 +467,6 @@ msgstr "Enter email address to reset password"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Enter email address..."
|
||||
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Enter your one-time password."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -554,12 +537,6 @@ msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "FreeBSD command"
|
||||
msgstr "FreeBSD command"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Full"
|
||||
@@ -663,7 +640,6 @@ msgid "Manage display and notification preferences."
|
||||
msgstr "Manage display and notification preferences."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr "Manual setup instructions"
|
||||
|
||||
@@ -699,8 +675,6 @@ msgid "Network traffic of docker containers"
|
||||
msgstr "Network traffic of docker containers"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Network traffic of public interfaces"
|
||||
msgstr "Network traffic of public interfaces"
|
||||
|
||||
@@ -736,10 +710,6 @@ 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/login/auth-form.tsx
|
||||
msgid "One-time password"
|
||||
msgstr "One-time password"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -858,14 +828,6 @@ msgstr "Read"
|
||||
msgid "Received"
|
||||
msgstr "Received"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Request a one-time password"
|
||||
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Request OTP"
|
||||
msgstr "Request OTP"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Reset Password"
|
||||
msgstr "Reset Password"
|
||||
@@ -921,6 +883,10 @@ msgstr "Sent"
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Set percentage thresholds for meter colors."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
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
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1031,10 +997,6 @@ msgstr "Throughput of {extraFsName}"
|
||||
msgid "Throughput of root filesystem"
|
||||
msgstr "Throughput of root filesystem"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Time format"
|
||||
msgstr "Time format"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "To email(s)"
|
||||
msgstr "To email(s)"
|
||||
@@ -1067,14 +1029,6 @@ msgstr "Tokens allow agents to connect and register. Fingerprints are stable ide
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data received for each interface"
|
||||
msgstr "Total data received for each interface"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data sent for each interface"
|
||||
msgstr "Total data sent for each interface"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
msgstr "Triggers when 1 minute load average exceeds a threshold"
|
||||
@@ -1136,10 +1090,6 @@ msgstr "Up"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Up ({upSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Uptime"
|
||||
|
||||
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
1698
internal/site/src/locales/he/he.po
Normal file
1698
internal/site/src/locales/he/he.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
1698
internal/site/src/locales/id/id.po
Normal file
1698
internal/site/src/locales/id/id.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,15 @@ msgstr ""
|
||||
"Language: is\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-09-22 23:10\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Icelandic\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: is\n"
|
||||
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 16\n"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -112,7 +112,7 @@ msgstr ""
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
@@ -173,6 +173,10 @@ msgstr "Meðal nýting örgjörva yfir allt kerfið"
|
||||
msgid "Average utilization of {0}"
|
||||
msgstr "Meðal notkun af {0}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of GPU engines"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Backups"
|
||||
@@ -197,7 +201,7 @@ msgstr "Beszel notar <0>Shoutrrr</0> til að tengjast vinsælum tilkynningaþjó
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
msgstr "Binary"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -568,7 +572,7 @@ msgstr ""
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Full"
|
||||
msgstr "Full"
|
||||
msgstr ""
|
||||
|
||||
#. Context: General settings
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -576,6 +580,10 @@ msgstr "Full"
|
||||
msgid "General"
|
||||
msgstr "Almennt"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Engines"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
msgstr "Skjákorts rafmagnsnotkun"
|
||||
@@ -592,7 +600,7 @@ msgstr "Homebrew skipun"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr "Host / IP"
|
||||
msgstr ""
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -697,7 +705,7 @@ msgstr "Nafn"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -837,7 +845,7 @@ msgstr "Vinsamlegast skráðu þig inn á aðganginn þinn"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -920,7 +928,7 @@ msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Sent"
|
||||
msgstr "Sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
@@ -1178,6 +1186,10 @@ msgstr ""
|
||||
msgid "View"
|
||||
msgstr "Skoða"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "View your 200 most recent alerts."
|
||||
msgstr ""
|
||||
@@ -1233,3 +1245,4 @@ msgstr ""
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Notenda stillingar vistaðar."
|
||||
|
||||
|
||||
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
1698
internal/site/src/locales/ro/ro.po
Normal file
1698
internal/site/src/locales/ro/ro.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
1698
internal/site/src/locales/sr/sr.po
Normal file
1698
internal/site/src/locales/sr/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1698
internal/site/src/locales/th/th.po
Normal file
1698
internal/site/src/locales/th/th.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
2
internal/site/src/types.d.ts
vendored
2
internal/site/src/types.d.ts
vendored
@@ -141,8 +141,6 @@ export interface SystemStats {
|
||||
g?: Record<string, GPUData>
|
||||
/** battery percent and state */
|
||||
bat?: [number, BatteryState]
|
||||
/** network interfaces [upload bytes, download bytes, total upload bytes, total download bytes] */
|
||||
ni?: Record<string, [number, number, number, number]>
|
||||
}
|
||||
|
||||
export interface GPUData {
|
||||
|
||||
@@ -125,28 +125,3 @@ func CreateSystems(app core.App, count int, userId string, status string) ([]*co
|
||||
}
|
||||
return systems, nil
|
||||
}
|
||||
|
||||
// GetHubWithUser creates a test hub with a test user and user settings
|
||||
func GetHubWithUser(t *testing.T) (*TestHub, *core.Record) {
|
||||
hub, err := NewTestHub(t.TempDir())
|
||||
assert.NoError(t, err)
|
||||
hub.StartHub()
|
||||
|
||||
// Manually initialize the system manager to bind event hooks
|
||||
err = hub.GetSystemManager().Initialize()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a test user
|
||||
user, err := CreateUser(hub, "test@example.com", "password")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create user settings for the test user (required for alert notifications)
|
||||
userSettingsData := map[string]any{
|
||||
"user": user.Id,
|
||||
"settings": `{"emails":[test@example.com],"webhooks":[]}`,
|
||||
}
|
||||
_, err = CreateRecord(hub, "user_settings", userSettingsData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return hub, user
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
## 0.12.8
|
||||
|
||||
- Add per-interface network traffic charts. (#926)
|
||||
|
||||
- Add cumulative network traffic charts. (#926)
|
||||
|
||||
- Add setting for time format (12h / 24h). (#424)
|
||||
|
||||
- Add experimental one-time password (OTP) support.
|
||||
|
||||
- Add `TRUSTED_AUTH_HEADER` environment variable for authentication forwarding. (#399)
|
||||
|
||||
- Add `AUTO_LOGIN` environment variable for automatic login. (#399)
|
||||
|
||||
- Add FreeBSD support for agent install script and update command.
|
||||
|
||||
## 0.12.7
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "beszel.fullname" . }}
|
||||
name: {{ include "beszel.fullname" . }}-web
|
||||
labels:
|
||||
{{- include "beszel.labels" . | nindent 4 }}
|
||||
{{- if .Values.service.annotations }}
|
||||
|
||||
@@ -30,10 +30,14 @@ securityContext: {}
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
loadBalancerIP: ""
|
||||
type: LoadBalancer
|
||||
loadBalancerIP: "10.0.10.251"
|
||||
port: 8090
|
||||
# -- Annotations for the DHCP service
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: pool
|
||||
metallb.universe.tf/allow-shared-ip: beszel-hub-web
|
||||
# -- Labels for the DHCP service
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
@@ -92,7 +96,7 @@ persistentVolumeClaim:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
storageClass: ""
|
||||
storageClass: "retain-local-path"
|
||||
|
||||
# -- volume claim size
|
||||
size: "500Mi"
|
||||
|
||||
Reference in New Issue
Block a user