Compare commits

...

210 Commits

Author SHA1 Message Date
hank
afd7385a4b New translations en.po (Chinese Traditional, Hong Kong) 2025-07-12 22:50:23 -04:00
hank
c0737f8696 New translations en.po (Croatian) 2025-07-12 22:50:22 -04:00
hank
039639d5db New translations en.po (Persian) 2025-07-12 22:50:21 -04:00
hank
ab7a18a79c New translations en.po (Icelandic) 2025-07-12 22:50:20 -04:00
hank
6726702a9f New translations en.po (Vietnamese) 2025-07-12 22:50:19 -04:00
hank
7cdee9cb5e New translations en.po (Chinese Traditional) 2025-07-12 22:50:18 -04:00
hank
c583214578 New translations en.po (Chinese Simplified) 2025-07-12 22:50:17 -04:00
hank
9fb023d54b New translations en.po (Ukrainian) 2025-07-12 22:50:16 -04:00
hank
492485b347 New translations en.po (Turkish) 2025-07-12 22:50:15 -04:00
hank
c9d4115268 New translations en.po (Swedish) 2025-07-12 22:50:14 -04:00
hank
7c814f24cf New translations en.po (Slovenian) 2025-07-12 22:50:13 -04:00
hank
30d89ad537 New translations en.po (Russian) 2025-07-12 22:50:12 -04:00
hank
5e02b76f93 New translations en.po (Portuguese) 2025-07-12 22:50:11 -04:00
hank
f82c4d7bdf New translations en.po (Polish) 2025-07-12 22:50:10 -04:00
hank
04f4221ab1 New translations en.po (Norwegian) 2025-07-12 22:50:09 -04:00
hank
6c231eb02f New translations en.po (Dutch) 2025-07-12 22:50:08 -04:00
hank
017365bb55 New translations en.po (Korean) 2025-07-12 22:50:07 -04:00
hank
824149927e New translations en.po (Japanese) 2025-07-12 22:50:06 -04:00
hank
4c8f2452a8 New translations en.po (Italian) 2025-07-12 22:50:05 -04:00
hank
196cbd7f78 New translations en.po (Hungarian) 2025-07-12 22:50:04 -04:00
hank
3c9c5e7938 New translations en.po (Greek) 2025-07-12 22:50:03 -04:00
hank
67a35ec7ea New translations en.po (German) 2025-07-12 22:50:02 -04:00
hank
3a3516d05e New translations en.po (Danish) 2025-07-12 22:50:01 -04:00
hank
cf22a44e03 New translations en.po (Czech) 2025-07-12 22:50:00 -04:00
hank
fe3c40d528 New translations en.po (Bulgarian) 2025-07-12 22:49:59 -04:00
hank
efa74665db New translations en.po (Arabic) 2025-07-12 22:49:58 -04:00
hank
49d94321fd New translations en.po (Spanish) 2025-07-12 22:49:57 -04:00
hank
250aa61322 New translations en.po (French) 2025-07-12 22:49:56 -04:00
hank
39cf17d24b New translations en.po (Chinese Traditional) 2025-07-12 21:11:02 -04:00
hank
a2ea3d104e New translations en.po (Russian) 2025-07-10 02:22:39 -04:00
hank
ab0f5c765f New translations en.po (Ukrainian) 2025-07-09 07:48:50 -04:00
hank
c6a4903b51 New translations en.po (Russian) 2025-07-09 04:25:18 -04:00
hank
2e14315e3b New translations en.po (Chinese Traditional, Hong Kong) 2025-07-08 21:37:13 -04:00
hank
da3543017d New translations en.po (Croatian) 2025-07-08 21:37:12 -04:00
hank
e2b9144cef New translations en.po (Persian) 2025-07-08 21:37:11 -04:00
hank
d897325933 New translations en.po (Icelandic) 2025-07-08 21:37:10 -04:00
hank
3382ff89e7 New translations en.po (Vietnamese) 2025-07-08 21:37:09 -04:00
hank
d07dd668e6 New translations en.po (Chinese Traditional) 2025-07-08 21:37:09 -04:00
hank
652c6f3303 New translations en.po (Chinese Simplified) 2025-07-08 21:37:08 -04:00
hank
05134cb354 New translations en.po (Ukrainian) 2025-07-08 21:37:07 -04:00
hank
397fdfd68b New translations en.po (Turkish) 2025-07-08 21:37:06 -04:00
hank
278e066c6f New translations en.po (Swedish) 2025-07-08 21:37:05 -04:00
hank
6573a599f6 New translations en.po (Slovenian) 2025-07-08 21:37:04 -04:00
hank
059575dc83 New translations en.po (Russian) 2025-07-08 21:37:03 -04:00
hank
40f55f6959 New translations en.po (Portuguese) 2025-07-08 21:37:02 -04:00
hank
e586aeb17a New translations en.po (Polish) 2025-07-08 21:37:01 -04:00
hank
dc3f2fdb51 New translations en.po (Norwegian) 2025-07-08 21:37:00 -04:00
hank
a8bcb2022b New translations en.po (Dutch) 2025-07-08 21:36:59 -04:00
hank
9db2daa640 New translations en.po (Korean) 2025-07-08 21:36:58 -04:00
hank
0364c59ae2 New translations en.po (Japanese) 2025-07-08 21:36:57 -04:00
hank
741c83d476 New translations en.po (Italian) 2025-07-08 21:36:56 -04:00
hank
f061352ddc New translations en.po (Hungarian) 2025-07-08 21:36:55 -04:00
hank
fa77ddfffe New translations en.po (Greek) 2025-07-08 21:36:54 -04:00
hank
27f4a648d1 New translations en.po (German) 2025-07-08 21:36:53 -04:00
hank
d0ff1ccc16 New translations en.po (Danish) 2025-07-08 21:36:52 -04:00
hank
8edb928e50 New translations en.po (Czech) 2025-07-08 21:36:51 -04:00
hank
97e31dd5a9 New translations en.po (Bulgarian) 2025-07-08 21:36:50 -04:00
hank
181bdab303 New translations en.po (Arabic) 2025-07-08 21:36:49 -04:00
hank
30756bb079 New translations en.po (Spanish) 2025-07-08 21:36:48 -04:00
hank
a0540821db New translations en.po (French) 2025-07-08 21:36:47 -04:00
hank
1779a056c3 New translations en.po (Chinese Traditional, Hong Kong) 2025-07-08 20:09:24 -04:00
hank
258c70f2d8 New translations en.po (Croatian) 2025-07-08 20:09:23 -04:00
hank
ea50c711b2 New translations en.po (Persian) 2025-07-08 20:09:22 -04:00
hank
b30d957b38 New translations en.po (Icelandic) 2025-07-08 20:09:21 -04:00
hank
e6abfc6a5d New translations en.po (Vietnamese) 2025-07-08 20:09:20 -04:00
hank
7d356d6edb New translations en.po (Chinese Traditional) 2025-07-08 20:09:19 -04:00
hank
30766df9fa New translations en.po (Chinese Simplified) 2025-07-08 20:09:18 -04:00
hank
eaf992f3f1 New translations en.po (Ukrainian) 2025-07-08 20:09:17 -04:00
hank
27b81aa489 New translations en.po (Turkish) 2025-07-08 20:09:16 -04:00
hank
5a903f472e New translations en.po (Swedish) 2025-07-08 20:09:15 -04:00
hank
4985474b0f New translations en.po (Slovenian) 2025-07-08 20:09:14 -04:00
hank
0632db3c19 New translations en.po (Russian) 2025-07-08 20:09:13 -04:00
hank
6980a4ef86 New translations en.po (Portuguese) 2025-07-08 20:09:12 -04:00
hank
ee22bf8bcb New translations en.po (Polish) 2025-07-08 20:09:11 -04:00
hank
ae4c402b99 New translations en.po (Norwegian) 2025-07-08 20:09:10 -04:00
hank
7281094dc3 New translations en.po (Dutch) 2025-07-08 20:09:09 -04:00
hank
699e81eb1c New translations en.po (Korean) 2025-07-08 20:09:08 -04:00
hank
a6065b6a19 New translations en.po (Japanese) 2025-07-08 20:09:07 -04:00
hank
b278c92e54 New translations en.po (Italian) 2025-07-08 20:09:06 -04:00
hank
83d41ed1f9 New translations en.po (Hungarian) 2025-07-08 20:09:05 -04:00
hank
7fa39a120f New translations en.po (Greek) 2025-07-08 20:09:04 -04:00
hank
4b59748c0c New translations en.po (German) 2025-07-08 20:09:03 -04:00
hank
ab2ef5bcfc New translations en.po (Danish) 2025-07-08 20:09:02 -04:00
hank
2524b0d359 New translations en.po (Czech) 2025-07-08 20:09:01 -04:00
hank
f84b159309 New translations en.po (Bulgarian) 2025-07-08 20:09:00 -04:00
hank
d5e5f8678a New translations en.po (Arabic) 2025-07-08 20:08:59 -04:00
hank
c91e0a9faa New translations en.po (Spanish) 2025-07-08 20:08:58 -04:00
hank
c6a6a353a5 New translations en.po (French) 2025-07-08 20:08:57 -04:00
hank
7d1f8f5cae New translations en.po (Czech) 2025-05-28 04:23:36 -04:00
hank
64a5ef4dfe New translations en.po (Portuguese) 2025-05-26 23:09:11 -04:00
hank
030d2b6684 New translations en.po (Greek) 2025-05-26 17:03:45 -04:00
hank
938e87db8b New translations en.po (German) 2025-05-26 08:12:11 -04:00
hank
31ed898a58 New translations en.po (Greek) 2025-05-24 15:52:00 -04:00
hank
1b8ad81046 New translations en.po (Danish) 2025-05-23 16:31:05 -04:00
hank
ff3874491c New translations en.po (Polish) 2025-05-12 08:30:31 -04:00
hank
fe81553c6a New translations en.po (Greek) 2025-05-06 17:35:17 -04:00
hank
6e8b3f6265 New translations en.po (Korean) 2025-05-05 10:36:01 -04:00
hank
9c87911a7f New translations en.po (Arabic) 2025-05-05 10:35:59 -04:00
hank
082cbb36e7 New translations en.po (Chinese Traditional) 2025-05-03 05:12:43 -04:00
hank
8347cf0e5c New translations en.po (Arabic) 2025-05-02 20:24:44 -04:00
hank
abfafae0d9 New translations en.po (Korean) 2025-04-29 10:28:27 -04:00
hank
6496f35915 New translations en.po (Japanese) 2025-04-28 04:23:06 -04:00
hank
7fee3da2e8 New translations en.po (Ukrainian) 2025-04-27 07:20:55 -04:00
hank
443e203d9c New translations en.po (German) 2025-04-27 06:25:22 -04:00
hank
24875b5733 New translations en.po (Chinese Traditional, Hong Kong) 2025-04-26 17:53:34 -04:00
hank
66eaf45c79 New translations en.po (Croatian) 2025-04-26 17:53:33 -04:00
hank
2b9fb8b1ec New translations en.po (Persian) 2025-04-26 17:53:33 -04:00
hank
06e0ae4733 New translations en.po (Icelandic) 2025-04-26 17:53:32 -04:00
hank
4cc0bfeb0f New translations en.po (Vietnamese) 2025-04-26 17:53:31 -04:00
hank
320d0e5d97 New translations en.po (Chinese Traditional) 2025-04-26 17:53:30 -04:00
hank
67953bb6af New translations en.po (Chinese Simplified) 2025-04-26 17:53:29 -04:00
hank
123c57ded9 New translations en.po (Ukrainian) 2025-04-26 17:53:28 -04:00
hank
a7991ca184 New translations en.po (Turkish) 2025-04-26 17:53:27 -04:00
hank
1b01410533 New translations en.po (Swedish) 2025-04-26 17:53:26 -04:00
hank
1687919f31 New translations en.po (Slovenian) 2025-04-26 17:53:25 -04:00
hank
6046dbb727 New translations en.po (Russian) 2025-04-26 17:53:24 -04:00
hank
2505b16faa New translations en.po (Portuguese) 2025-04-26 17:53:23 -04:00
hank
c4352e65fb New translations en.po (Polish) 2025-04-26 17:53:22 -04:00
hank
7b2e9ccdcb New translations en.po (Norwegian) 2025-04-26 17:53:21 -04:00
hank
75a87cddbf New translations en.po (Korean) 2025-04-26 17:53:20 -04:00
hank
bda7b9b48d New translations en.po (Japanese) 2025-04-26 17:53:19 -04:00
hank
65d6ec1918 New translations en.po (Italian) 2025-04-26 17:53:18 -04:00
hank
0e16fca07f New translations en.po (Hungarian) 2025-04-26 17:53:17 -04:00
hank
b7e574f379 New translations en.po (German) 2025-04-26 17:53:16 -04:00
hank
113f1833e1 New translations en.po (Danish) 2025-04-26 17:53:15 -04:00
hank
a0d3e60d29 New translations en.po (Czech) 2025-04-26 17:53:14 -04:00
hank
df7f12bfec New translations en.po (Bulgarian) 2025-04-26 17:53:13 -04:00
hank
02e88d99e5 New translations en.po (Spanish) 2025-04-26 17:53:12 -04:00
hank
a11ceb36d1 New translations en.po (French) 2025-04-26 17:53:11 -04:00
hank
d3a373338d New translations en.po (Arabic) 2025-04-26 17:53:10 -04:00
hank
e1ef05da8c New translations en.po (Dutch) 2025-04-26 17:53:09 -04:00
hank
c01bfd9332 New translations en.po (Arabic) 2025-04-23 13:19:56 -04:00
hank
209e82aed2 New translations en.po (Dutch) 2025-04-14 09:04:03 -04:00
hank
c66e740d52 New translations en.po (Arabic) 2025-04-05 21:52:03 -04:00
hank
551ae49c2e New translations en.po (French) 2025-03-28 17:04:47 -04:00
hank
906daa6f2a New translations en.po (French) 2025-03-26 06:03:01 -04:00
hank
49e55943c1 New translations en.po (Russian) 2025-03-25 04:32:17 -04:00
hank
9e75cbc1ef New translations en.po (Italian) 2025-03-22 11:29:03 -04:00
hank
79190a2c51 New translations en.po (Norwegian) 2025-03-17 09:40:03 -04:00
hank
32960c7c35 New translations en.po (Chinese Simplified) 2025-03-15 04:13:35 -04:00
hank
cdff974a8a New translations en.po (Ukrainian) 2025-03-15 00:14:37 -04:00
hank
20a6ef129c New translations en.po (Czech) 2025-03-13 20:50:03 -04:00
hank
d1e5310f83 New translations en.po (Japanese) 2025-03-13 06:13:10 -04:00
hank
152173eab5 New translations en.po (Korean) 2025-03-07 05:06:11 -05:00
hank
c2aec69638 New translations en.po (Ukrainian) 2025-03-06 07:37:37 -05:00
hank
16c97fe77b New translations en.po (Chinese Traditional, Hong Kong) 2025-03-06 02:27:48 -05:00
hank
bed5bc2791 New translations en.po (Croatian) 2025-03-06 02:27:47 -05:00
hank
424c7aceff New translations en.po (Persian) 2025-03-06 02:27:46 -05:00
hank
8eb101e714 New translations en.po (Vietnamese) 2025-03-06 02:27:45 -05:00
hank
f2e69e2a80 New translations en.po (Chinese Traditional) 2025-03-06 02:27:44 -05:00
hank
41b4dbce98 New translations en.po (Chinese Simplified) 2025-03-06 02:27:43 -05:00
hank
c3432fc7b5 New translations en.po (Ukrainian) 2025-03-06 02:27:42 -05:00
hank
b167244e28 New translations en.po (Turkish) 2025-03-06 02:27:40 -05:00
hank
bcca6a5a9d New translations en.po (Swedish) 2025-03-06 02:27:39 -05:00
hank
6b661b4878 New translations en.po (Slovenian) 2025-03-06 02:27:37 -05:00
hank
bc537edb73 New translations en.po (Russian) 2025-03-06 02:27:36 -05:00
hank
561a3e8aaf New translations en.po (Polish) 2025-03-06 02:27:34 -05:00
hank
f173ea37da New translations en.po (Norwegian) 2025-03-06 02:27:33 -05:00
hank
0d9f2ba06a New translations en.po (Dutch) 2025-03-06 02:27:32 -05:00
hank
4e772e6008 New translations en.po (Korean) 2025-03-06 02:27:31 -05:00
hank
b959d97502 New translations en.po (Italian) 2025-03-06 02:27:30 -05:00
hank
735cbe2cf0 New translations en.po (German) 2025-03-06 02:27:29 -05:00
hank
0f5a470495 New translations en.po (Danish) 2025-03-06 02:27:28 -05:00
hank
a42e99370e New translations en.po (Czech) 2025-03-06 02:27:27 -05:00
hank
abfae78af1 New translations en.po (Bulgarian) 2025-03-06 02:27:26 -05:00
hank
e36c40c4a9 New translations en.po (Arabic) 2025-03-06 02:27:25 -05:00
hank
bf7b2ae598 New translations en.po (Spanish) 2025-03-06 02:27:24 -05:00
hank
f71ee4b058 New translations en.po (French) 2025-03-06 02:27:23 -05:00
hank
f2466eb37d New translations en.po (Hungarian) 2025-03-06 02:27:22 -05:00
hank
7244c7130b New translations en.po (Japanese) 2025-03-06 02:27:21 -05:00
hank
f2e84a9d3e New translations en.po (Portuguese) 2025-03-06 02:27:20 -05:00
hank
04a1ee5e4e New translations en.po (Icelandic) 2025-03-06 02:27:19 -05:00
hank
9b83088897 New translations en.po (Norwegian) 2025-03-05 15:49:42 -05:00
hank
13b30aa255 New translations en.po (Russian) 2025-03-01 13:24:35 -05:00
hank
2bd25e9e8d New translations en.po (Ukrainian) 2025-03-01 08:20:09 -05:00
hank
4fe192bf28 New translations en.po (Ukrainian) 2025-03-01 07:11:52 -05:00
hank
2056ae285f New translations en.po (Spanish) 2025-02-28 12:20:35 -05:00
hank
a02e7a0a69 New translations en.po (Spanish) 2025-02-28 11:07:32 -05:00
hank
b1a9e90034 New translations en.po (Chinese Traditional, Hong Kong) 2025-02-27 17:31:32 -05:00
hank
20916fab3e New translations en.po (Croatian) 2025-02-27 17:31:31 -05:00
hank
0c777cca72 New translations en.po (Persian) 2025-02-27 17:31:31 -05:00
hank
44e30ad429 New translations en.po (Vietnamese) 2025-02-27 17:31:30 -05:00
hank
9e30786dda New translations en.po (Chinese Traditional) 2025-02-27 17:31:29 -05:00
hank
1f677773e7 New translations en.po (Chinese Simplified) 2025-02-27 17:31:28 -05:00
hank
882289da91 New translations en.po (Ukrainian) 2025-02-27 17:31:26 -05:00
hank
fbbc4eff27 New translations en.po (Turkish) 2025-02-27 17:31:25 -05:00
hank
b78231f677 New translations en.po (Swedish) 2025-02-27 17:31:24 -05:00
hank
332e2d14a9 New translations en.po (Slovenian) 2025-02-27 17:31:23 -05:00
hank
e088b88c84 New translations en.po (Russian) 2025-02-27 17:31:22 -05:00
hank
09840f95d9 New translations en.po (Polish) 2025-02-27 17:31:21 -05:00
hank
de03bb658a New translations en.po (Norwegian) 2025-02-27 17:31:19 -05:00
hank
bcadfeb729 New translations en.po (Dutch) 2025-02-27 17:31:18 -05:00
hank
ec582a9171 New translations en.po (Korean) 2025-02-27 17:31:17 -05:00
hank
0c3eaefc90 New translations en.po (Italian) 2025-02-27 17:31:16 -05:00
hank
d6b5866f90 New translations en.po (German) 2025-02-27 17:31:15 -05:00
hank
2ba629e4b4 New translations en.po (Danish) 2025-02-27 17:31:14 -05:00
hank
6e9341b7ff New translations en.po (Czech) 2025-02-27 17:31:13 -05:00
hank
89499a341b New translations en.po (Bulgarian) 2025-02-27 17:31:12 -05:00
hank
9c88845798 New translations en.po (Arabic) 2025-02-27 17:31:10 -05:00
hank
c9f65f63e6 New translations en.po (Spanish) 2025-02-27 17:31:09 -05:00
hank
996620c8e0 New translations en.po (French) 2025-02-27 17:31:08 -05:00
hank
626c1358d9 New translations en.po (Hungarian) 2025-02-27 17:31:07 -05:00
hank
19dd39b7db New translations en.po (Japanese) 2025-02-27 17:31:06 -05:00
hank
d482b50e31 New translations en.po (Portuguese) 2025-02-27 17:31:05 -05:00
hank
f61ec49c76 New translations en.po (Icelandic) 2025-02-27 17:31:03 -05:00
henrygd
2b43ba3cbe i18n: update language files 2025-02-27 17:20:12 -05:00
ArsFy
b2b1a0b6ea i18n: new Chinese translations 2025-02-27 17:19:04 -05:00
stanol
b11d0aae61 i18n: new Ukrainian translations 2025-02-27 17:18:35 -05:00
henrygd
2b73d8845a feat: allow x min downtime before alerting (#595, #625)
- splits alerts package into three files. status alerts were not
modified aside from updating to slices.Delete method
2025-02-27 17:12:25 -05:00
henrygd
41e3e3d760 chore: update .gitignore 2025-02-27 00:37:29 -05:00
37 changed files with 11702 additions and 7490 deletions

3
.gitignore vendored
View File

@@ -14,4 +14,5 @@ node_modules
beszel/build
*timestamp*
.swc
beszel/site/src/locales/**/*.ts
beszel/site/src/locales/**/*.ts
*.bak

View File

@@ -2,25 +2,24 @@
package alerts
import (
"beszel/internal/entities/system"
"fmt"
"net/mail"
"net/url"
"strings"
"sync"
"time"
"github.com/containrrr/shoutrrr"
"github.com/goccy/go-json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
type AlertManager struct {
app core.App
app core.App
alertQueue chan alertTask
stopChan chan struct{}
pendingAlerts sync.Map
}
type AlertMessageData struct {
@@ -60,350 +59,43 @@ type SystemAlertData struct {
descriptor string // override descriptor in notification body (for temp sensor, disk partition, etc)
}
// notification services that support title param
var supportsTitle = map[string]struct{}{
"bark": {},
"discord": {},
"gotify": {},
"ifttt": {},
"join": {},
"matrix": {},
"ntfy": {},
"opsgenie": {},
"pushbullet": {},
"pushover": {},
"slack": {},
"teams": {},
"telegram": {},
"zulip": {},
}
// NewAlertManager creates a new AlertManager instance.
func NewAlertManager(app core.App) *AlertManager {
return &AlertManager{
app: app,
am := &AlertManager{
app: app,
alertQueue: make(chan alertTask),
stopChan: make(chan struct{}),
}
go am.startWorker()
return am
}
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, systemInfo system.Info, temperatures map[string]float64, extraFs map[string]*system.FsStats) error {
// start := time.Now()
// defer func() {
// log.Println("alert stats took", time.Since(start))
// }()
alertRecords, err := am.app.FindAllRecords("alerts",
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
)
if err != nil || len(alertRecords) == 0 {
// log.Println("no alerts found for system")
return nil
}
var validAlerts []SystemAlertData
now := systemRecord.GetDateTime("updated").Time().UTC()
oldestTime := now
for _, alertRecord := range alertRecords {
name := alertRecord.GetString("name")
var val float64
unit := "%"
switch name {
case "CPU":
val = systemInfo.Cpu
case "Memory":
val = systemInfo.MemPct
case "Bandwidth":
val = systemInfo.Bandwidth
unit = " MB/s"
case "Disk":
maxUsedPct := systemInfo.DiskPct
for _, fs := range extraFs {
usedPct := fs.DiskUsed / fs.DiskTotal * 100
if usedPct > maxUsedPct {
maxUsedPct = usedPct
}
}
val = maxUsedPct
case "Temperature":
if temperatures == nil {
continue
}
for _, temp := range temperatures {
if temp > val {
val = temp
}
}
unit = "°C"
}
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
// CONTINUE
// IF alert is not triggered and curValue is less than threshold
// OR alert is triggered and curValue is greater than threshold
if (!triggered && val <= threshold) || (triggered && val > threshold) {
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
continue
}
min := max(1, cast.ToUint8(alertRecord.Get("min")))
// add time to alert time to make sure it's slighty after record creation
time := now.Add(-time.Duration(min) * time.Minute)
if time.Before(oldestTime) {
oldestTime = time
}
validAlerts = append(validAlerts, SystemAlertData{
systemRecord: systemRecord,
alertRecord: alertRecord,
name: name,
unit: unit,
val: val,
threshold: threshold,
triggered: triggered,
time: time,
min: min,
})
}
systemStats := []struct {
Stats []byte `db:"stats"`
Created types.DateTime `db:"created"`
}{}
err = am.app.DB().
Select("stats", "created").
From("system_stats").
Where(dbx.NewExp(
"system={:system} AND type='1m' AND created > {:created}",
dbx.Params{
"system": systemRecord.Id,
// subtract some time to give us a bit of buffer
"created": oldestTime.Add(-time.Second * 90),
},
)).
OrderBy("created").
All(&systemStats)
if err != nil {
return err
}
// get oldest record creation time from first record in the slice
oldestRecordTime := systemStats[0].Created.Time()
// log.Println("oldestRecordTime", oldestRecordTime.String())
// delete from validAlerts if time is older than oldestRecord
for i := 0; i < len(validAlerts); i++ {
if validAlerts[i].time.Before(oldestRecordTime) {
// log.Println("deleting alert - time is older than oldestRecord", validAlerts[i].name, oldestRecordTime, validAlerts[i].time)
validAlerts = append(validAlerts[:i], validAlerts[i+1:]...)
}
}
if len(validAlerts) == 0 {
// log.Println("no valid alerts found")
return nil
}
var stats SystemAlertStats
// we can skip the latest systemStats record since it's the current value
for i := 0; i < len(systemStats); i++ {
stat := systemStats[i]
// subtract 10 seconds to give a small time buffer
systemStatsCreation := stat.Created.Time().Add(-time.Second * 10)
if err := json.Unmarshal(stat.Stats, &stats); err != nil {
return err
}
// log.Println("stats", stats)
for j := range validAlerts {
alert := &validAlerts[j]
// reset alert val on first iteration
if i == 0 {
alert.val = 0
}
// continue if system_stats is older than alert time range
if systemStatsCreation.Before(alert.time) {
continue
}
// add to alert value
switch alert.name {
case "CPU":
alert.val += stats.Cpu
case "Memory":
alert.val += stats.Mem
case "Bandwidth":
alert.val += stats.NetSent + stats.NetRecv
case "Disk":
if alert.mapSums == nil {
alert.mapSums = make(map[string]float32, len(extraFs)+1)
}
// add root disk
if _, ok := alert.mapSums["root"]; !ok {
alert.mapSums["root"] = 0.0
}
alert.mapSums["root"] += float32(stats.Disk)
// add extra disks
for key, fs := range extraFs {
if _, ok := alert.mapSums[key]; !ok {
alert.mapSums[key] = 0.0
}
alert.mapSums[key] += float32(fs.DiskUsed / fs.DiskTotal * 100)
}
case "Temperature":
if alert.mapSums == nil {
alert.mapSums = make(map[string]float32, len(stats.Temperatures))
}
for key, temp := range stats.Temperatures {
if _, ok := alert.mapSums[key]; !ok {
alert.mapSums[key] = float32(0)
}
alert.mapSums[key] += temp
}
default:
continue
}
alert.count++
}
}
// sum up vals for each alert
for _, alert := range validAlerts {
switch alert.name {
case "Disk":
maxPct := float32(0)
for key, value := range alert.mapSums {
sumPct := float32(value)
if sumPct > maxPct {
maxPct = sumPct
alert.descriptor = fmt.Sprintf("Usage of %s", key)
}
}
alert.val = float64(maxPct / float32(alert.count))
case "Temperature":
maxTemp := float32(0)
for key, value := range alert.mapSums {
sumTemp := float32(value) / float32(alert.count)
if sumTemp > maxTemp {
maxTemp = sumTemp
alert.descriptor = fmt.Sprintf("Highest sensor %s", key)
}
}
alert.val = float64(maxTemp)
default:
alert.val = alert.val / float64(alert.count)
}
minCount := float32(alert.min) / 1.2
// log.Println("alert", alert.name, "val", alert.val, "threshold", alert.threshold, "triggered", alert.triggered)
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
// pass through alert if count is greater than or equal to minCount
if float32(alert.count) >= minCount {
if !alert.triggered && alert.val > alert.threshold {
alert.triggered = true
go am.sendSystemAlert(alert)
} else if alert.triggered && alert.val <= alert.threshold {
alert.triggered = false
go am.sendSystemAlert(alert)
}
}
}
return nil
}
func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
// log.Printf("Sending alert %s: val %f | count %d | threshold %f\n", alert.name, alert.val, alert.count, alert.threshold)
systemName := alert.systemRecord.GetString("name")
// change Disk to Disk usage
if alert.name == "Disk" {
alert.name += " usage"
}
// make title alert name lowercase if not CPU
titleAlertName := alert.name
if titleAlertName != "CPU" {
titleAlertName = strings.ToLower(titleAlertName)
}
var subject string
if alert.triggered {
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
} else {
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
}
minutesLabel := "minute"
if alert.min > 1 {
minutesLabel += "s"
}
if alert.descriptor == "" {
alert.descriptor = alert.name
}
body := fmt.Sprintf("%s averaged %.2f%s for the previous %v %s.", alert.descriptor, alert.val, alert.unit, alert.min, minutesLabel)
alert.alertRecord.Set("triggered", alert.triggered)
if err := am.app.Save(alert.alertRecord); err != nil {
// app.Logger().Error("failed to save alert record", "err", err.Error())
return
}
// expand the user relation and send the alert
if errs := am.app.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
return
}
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
am.sendAlert(AlertMessageData{
UserID: user.Id,
Title: subject,
Message: body,
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
LinkText: "View " + systemName,
})
}
}
// todo: allow x minutes downtime before sending alert
func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *core.Record) error {
var alertStatus string
switch newStatus {
case "up":
if oldSystemRecord.GetString("status") == "down" {
alertStatus = "up"
}
case "down":
if oldSystemRecord.GetString("status") == "up" {
alertStatus = "down"
}
}
if alertStatus == "" {
return nil
}
// check if use
alertRecords, err := am.app.FindAllRecords("alerts",
dbx.HashExp{
"system": oldSystemRecord.Id,
"name": "Status",
},
)
if err != nil || len(alertRecords) == 0 {
// log.Println("no alerts found for system")
return nil
}
for _, alertRecord := range alertRecords {
// expand the user relation
if errs := am.app.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
return fmt.Errorf("failed to expand: %v", errs)
}
user := alertRecord.ExpandedOne("user")
if user == nil {
return nil
}
emoji := "\U0001F534"
if alertStatus == "up" {
emoji = "\u2705"
}
// send alert
systemName := oldSystemRecord.GetString("name")
am.sendAlert(AlertMessageData{
UserID: user.Id,
Title: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
Message: fmt.Sprintf("Connection to %s is %s", systemName, alertStatus),
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
LinkText: "View " + systemName,
})
}
return nil
}
func (am *AlertManager) sendAlert(data AlertMessageData) {
func (am *AlertManager) SendAlert(data AlertMessageData) error {
// get user settings
record, err := am.app.FindFirstRecordByFilter(
"user_settings", "user={:user}",
dbx.Params{"user": data.UserID},
)
if err != nil {
am.app.Logger().Error("Failed to get user settings", "err", err.Error())
return
return err
}
// unmarshal user settings
userAlertSettings := UserNotificationSettings{
@@ -421,8 +113,7 @@ func (am *AlertManager) sendAlert(data AlertMessageData) {
}
// send alerts via email
if len(userAlertSettings.Emails) == 0 {
// log.Println("No email addresses found")
return
return nil
}
addresses := []mail.Address{}
for _, email := range userAlertSettings.Emails {
@@ -437,18 +128,16 @@ func (am *AlertManager) sendAlert(data AlertMessageData) {
Name: am.app.Settings().Meta.SenderName,
},
}
if err := am.app.NewMailClient().Send(&message); err != nil {
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
} else {
am.app.Logger().Info("Sent email alert", "to", message.To, "subj", message.Subject)
err = am.app.NewMailClient().Send(&message)
if err != nil {
return err
}
am.app.Logger().Info("Sent email alert", "to", message.To, "subj", message.Subject)
return nil
}
// SendShoutrrrAlert sends an alert via a Shoutrrr URL
func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error {
// services that support title param
supportsTitle := []string{"bark", "discord", "gotify", "ifttt", "join", "matrix", "ntfy", "opsgenie", "pushbullet", "pushover", "slack", "teams", "telegram", "zulip"}
// Parse the URL
parsedURL, err := url.Parse(notificationUrl)
if err != nil {
@@ -458,7 +147,7 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
queryParams := parsedURL.Query()
// Add title
if sliceContains(supportsTitle, scheme) {
if _, ok := supportsTitle[scheme]; ok {
queryParams.Add("title", title)
} else if scheme == "mattermost" {
// use markdown title for mattermost
@@ -499,16 +188,6 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
return nil
}
// Contains checks if a string is present in a slice of strings
func sliceContains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {
info, _ := e.RequestInfo()
if info.Auth == nil {

View File

@@ -0,0 +1,175 @@
package alerts
import (
"fmt"
"net/url"
"strings"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
type alertTask struct {
action string // "schedule" or "cancel"
systemName string
alertRecord *core.Record
delay time.Duration
}
type alertInfo struct {
systemName string
alertRecord *core.Record
expireTime time.Time
}
// startWorker is a long-running goroutine that processes alert tasks
// every x seconds. It must be running to process status alerts.
func (am *AlertManager) startWorker() {
// no special reason for 13 seconds
tick := time.Tick(13 * time.Second)
for {
select {
case <-am.stopChan:
return
case task := <-am.alertQueue:
switch task.action {
case "schedule":
am.pendingAlerts.Store(task.alertRecord.Id, &alertInfo{
systemName: task.systemName,
alertRecord: task.alertRecord,
expireTime: time.Now().Add(task.delay),
})
case "cancel":
am.pendingAlerts.Delete(task.alertRecord.Id)
}
case <-tick:
// Check for expired alerts every tick
now := time.Now()
for key, value := range am.pendingAlerts.Range {
info := value.(*alertInfo)
if now.After(info.expireTime) {
// Downtime delay has passed, process alert
am.sendStatusAlert("down", info.systemName, info.alertRecord)
am.pendingAlerts.Delete(key)
}
}
}
}
}
// StopWorker shuts down the AlertManager.worker goroutine
func (am *AlertManager) StopWorker() {
close(am.stopChan)
}
// HandleStatusAlerts manages the logic when system status changes.
func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *core.Record) error {
switch newStatus {
case "up":
if oldSystemRecord.GetString("status") != "down" {
return nil
}
case "down":
if oldSystemRecord.GetString("status") != "up" {
return nil
}
default:
return nil
}
alertRecords, err := am.getSystemStatusAlerts(oldSystemRecord.Id)
if err != nil {
return err
}
if len(alertRecords) == 0 {
return nil
}
systemName := oldSystemRecord.GetString("name")
if newStatus == "down" {
am.handleSystemDown(systemName, alertRecords)
} else {
am.handleSystemUp(systemName, alertRecords)
}
return nil
}
// getSystemStatusAlerts retrieves all "Status" alert records for a given system ID.
func (am *AlertManager) getSystemStatusAlerts(systemID string) ([]*core.Record, error) {
alertRecords, err := am.app.FindAllRecords("alerts", dbx.HashExp{
"system": systemID,
"name": "Status",
})
if err != nil {
return nil, err
}
return alertRecords, nil
}
// Schedules delayed "down" alerts for each alert record.
func (am *AlertManager) handleSystemDown(systemName string, alertRecords []*core.Record) {
for _, alertRecord := range alertRecords {
// Continue if alert is already scheduled
if _, exists := am.pendingAlerts.Load(alertRecord.Id); exists {
continue
}
// Schedule by adding to queue
min := max(1, alertRecord.GetInt("min"))
am.alertQueue <- alertTask{
action: "schedule",
systemName: systemName,
alertRecord: alertRecord,
delay: time.Duration(min) * time.Minute,
}
}
}
// handleSystemUp manages the logic when a system status changes to "up".
// It cancels any pending alerts and sends "up" alerts.
func (am *AlertManager) handleSystemUp(systemName string, alertRecords []*core.Record) {
for _, alertRecord := range alertRecords {
alertRecordID := alertRecord.Id
// If alert exists for record, delete and continue (down alert not sent)
if _, exists := am.pendingAlerts.Load(alertRecordID); exists {
am.alertQueue <- alertTask{
action: "cancel",
alertRecord: alertRecord,
}
continue
}
// No alert scheduled for this record, send "up" alert
if err := am.sendStatusAlert("up", systemName, alertRecord); err != nil {
am.app.Logger().Error("Failed to send alert", "err", err.Error())
}
}
}
// sendStatusAlert sends a status alert ("up" or "down") to the users associated with the alert records.
func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, alertRecord *core.Record) error {
var emoji string
if alertStatus == "up" {
emoji = "\u2705" // Green checkmark emoji
} else {
emoji = "\U0001F534" // Red alert emoji
}
title := fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji)
message := strings.TrimSuffix(title, emoji)
if errs := am.app.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
return errs["user"]
}
user := alertRecord.ExpandedOne("user")
if user == nil {
return nil
}
return am.SendAlert(AlertMessageData{
UserID: user.Id,
Title: title,
Message: message,
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
LinkText: "View " + systemName,
})
}

View File

@@ -0,0 +1,288 @@
package alerts
import (
"beszel/internal/entities/system"
"fmt"
"net/url"
"slices"
"strings"
"time"
"github.com/goccy/go-json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, systemInfo system.Info, temperatures map[string]float64, extraFs map[string]*system.FsStats) error {
alertRecords, err := am.app.FindAllRecords("alerts",
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
)
if err != nil || len(alertRecords) == 0 {
// log.Println("no alerts found for system")
return nil
}
var validAlerts []SystemAlertData
now := systemRecord.GetDateTime("updated").Time().UTC()
oldestTime := now
for _, alertRecord := range alertRecords {
name := alertRecord.GetString("name")
var val float64
unit := "%"
switch name {
case "CPU":
val = systemInfo.Cpu
case "Memory":
val = systemInfo.MemPct
case "Bandwidth":
val = systemInfo.Bandwidth
unit = " MB/s"
case "Disk":
maxUsedPct := systemInfo.DiskPct
for _, fs := range extraFs {
usedPct := fs.DiskUsed / fs.DiskTotal * 100
if usedPct > maxUsedPct {
maxUsedPct = usedPct
}
}
val = maxUsedPct
case "Temperature":
if temperatures == nil {
continue
}
for _, temp := range temperatures {
if temp > val {
val = temp
}
}
unit = "°C"
}
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
// CONTINUE
// IF alert is not triggered and curValue is less than threshold
// OR alert is triggered and curValue is greater than threshold
if (!triggered && val <= threshold) || (triggered && val > threshold) {
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
continue
}
min := max(1, cast.ToUint8(alertRecord.Get("min")))
// add time to alert time to make sure it's slighty after record creation
time := now.Add(-time.Duration(min) * time.Minute)
if time.Before(oldestTime) {
oldestTime = time
}
validAlerts = append(validAlerts, SystemAlertData{
systemRecord: systemRecord,
alertRecord: alertRecord,
name: name,
unit: unit,
val: val,
threshold: threshold,
triggered: triggered,
time: time,
min: min,
})
}
systemStats := []struct {
Stats []byte `db:"stats"`
Created types.DateTime `db:"created"`
}{}
err = am.app.DB().
Select("stats", "created").
From("system_stats").
Where(dbx.NewExp(
"system={:system} AND type='1m' AND created > {:created}",
dbx.Params{
"system": systemRecord.Id,
// subtract some time to give us a bit of buffer
"created": oldestTime.Add(-time.Second * 90),
},
)).
OrderBy("created").
All(&systemStats)
if err != nil {
return err
}
// get oldest record creation time from first record in the slice
oldestRecordTime := systemStats[0].Created.Time()
// log.Println("oldestRecordTime", oldestRecordTime.String())
// delete from validAlerts if time is older than oldestRecord
for i := range validAlerts {
if validAlerts[i].time.Before(oldestRecordTime) {
// log.Println("deleting alert - time is older than oldestRecord", validAlerts[i].name, oldestRecordTime, validAlerts[i].time)
validAlerts = slices.Delete(validAlerts, i, i+1)
}
}
if len(validAlerts) == 0 {
// log.Println("no valid alerts found")
return nil
}
var stats SystemAlertStats
// we can skip the latest systemStats record since it's the current value
for i := range systemStats {
stat := systemStats[i]
// subtract 10 seconds to give a small time buffer
systemStatsCreation := stat.Created.Time().Add(-time.Second * 10)
if err := json.Unmarshal(stat.Stats, &stats); err != nil {
return err
}
// log.Println("stats", stats)
for j := range validAlerts {
alert := &validAlerts[j]
// reset alert val on first iteration
if i == 0 {
alert.val = 0
}
// continue if system_stats is older than alert time range
if systemStatsCreation.Before(alert.time) {
continue
}
// add to alert value
switch alert.name {
case "CPU":
alert.val += stats.Cpu
case "Memory":
alert.val += stats.Mem
case "Bandwidth":
alert.val += stats.NetSent + stats.NetRecv
case "Disk":
if alert.mapSums == nil {
alert.mapSums = make(map[string]float32, len(extraFs)+1)
}
// add root disk
if _, ok := alert.mapSums["root"]; !ok {
alert.mapSums["root"] = 0.0
}
alert.mapSums["root"] += float32(stats.Disk)
// add extra disks
for key, fs := range extraFs {
if _, ok := alert.mapSums[key]; !ok {
alert.mapSums[key] = 0.0
}
alert.mapSums[key] += float32(fs.DiskUsed / fs.DiskTotal * 100)
}
case "Temperature":
if alert.mapSums == nil {
alert.mapSums = make(map[string]float32, len(stats.Temperatures))
}
for key, temp := range stats.Temperatures {
if _, ok := alert.mapSums[key]; !ok {
alert.mapSums[key] = float32(0)
}
alert.mapSums[key] += temp
}
default:
continue
}
alert.count++
}
}
// sum up vals for each alert
for _, alert := range validAlerts {
switch alert.name {
case "Disk":
maxPct := float32(0)
for key, value := range alert.mapSums {
sumPct := float32(value)
if sumPct > maxPct {
maxPct = sumPct
alert.descriptor = fmt.Sprintf("Usage of %s", key)
}
}
alert.val = float64(maxPct / float32(alert.count))
case "Temperature":
maxTemp := float32(0)
for key, value := range alert.mapSums {
sumTemp := float32(value) / float32(alert.count)
if sumTemp > maxTemp {
maxTemp = sumTemp
alert.descriptor = fmt.Sprintf("Highest sensor %s", key)
}
}
alert.val = float64(maxTemp)
default:
alert.val = alert.val / float64(alert.count)
}
minCount := float32(alert.min) / 1.2
// log.Println("alert", alert.name, "val", alert.val, "threshold", alert.threshold, "triggered", alert.triggered)
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
// pass through alert if count is greater than or equal to minCount
if float32(alert.count) >= minCount {
if !alert.triggered && alert.val > alert.threshold {
alert.triggered = true
go am.sendSystemAlert(alert)
} else if alert.triggered && alert.val <= alert.threshold {
alert.triggered = false
go am.sendSystemAlert(alert)
}
}
}
return nil
}
func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
// log.Printf("Sending alert %s: val %f | count %d | threshold %f\n", alert.name, alert.val, alert.count, alert.threshold)
systemName := alert.systemRecord.GetString("name")
// change Disk to Disk usage
if alert.name == "Disk" {
alert.name += " usage"
}
// make title alert name lowercase if not CPU
titleAlertName := alert.name
if titleAlertName != "CPU" {
titleAlertName = strings.ToLower(titleAlertName)
}
var subject string
if alert.triggered {
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
} else {
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
}
minutesLabel := "minute"
if alert.min > 1 {
minutesLabel += "s"
}
if alert.descriptor == "" {
alert.descriptor = alert.name
}
body := fmt.Sprintf("%s averaged %.2f%s for the previous %v %s.", alert.descriptor, alert.val, alert.unit, alert.min, minutesLabel)
alert.alertRecord.Set("triggered", alert.triggered)
if err := am.app.Save(alert.alertRecord); err != nil {
// app.Logger().Error("failed to save alert record", "err", err.Error())
return
}
// expand the user relation and send the alert
if errs := am.app.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
return
}
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
am.SendAlert(AlertMessageData{
UserID: user.Id,
Title: subject,
Message: body,
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
LinkText: "View " + systemName,
})
}
}

View File

@@ -160,13 +160,11 @@ export function SystemAlertGlobal({
function AlertContent({ data }: { data: AlertData }) {
const { key } = data
const hasSliders = !("single" in data.alert)
const singleDescription = data.alert.singleDesc?.()
const [checked, setChecked] = useState(data.checked || false)
const [min, setMin] = useState(data.min || (hasSliders ? 10 : 0))
const [value, setValue] = useState(data.val || (hasSliders ? 80 : 0))
const showSliders = checked && hasSliders
const [min, setMin] = useState(data.min || 10)
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
const newMin = useRef(min)
const newValue = useRef(value)
@@ -180,14 +178,14 @@ function AlertContent({ data }: { data: AlertData }) {
<label
htmlFor={`s${key}`}
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
"pb-0": showSliders,
"pb-0": checked,
})}
>
<div className="grid gap-1 select-none">
<p className="font-semibold flex gap-3 items-center">
<Icon className="h-4 w-4 opacity-85" /> {data.alert.name()}
</p>
{!showSliders && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
</div>
<Switch
id={`s${key}`}
@@ -198,9 +196,10 @@ function AlertContent({ data }: { data: AlertData }) {
}}
/>
</label>
{showSliders && (
{checked && (
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
<Suspense fallback={<div className="h-10" />}>
{!singleDescription && (
<div>
<p id={`v${key}`} className="text-sm block h-8">
<Trans>
@@ -222,8 +221,12 @@ function AlertContent({ data }: { data: AlertData }) {
/>
</div>
</div>
<div>
<p id={`t${key}`} className="text-sm block h-8">
)}
<div className={cn(singleDescription && "col-span-full lowercase")}>
<p id={`t${key}`} className="text-sm block h-8 first-letter:uppercase">
{singleDescription && (
<>{singleDescription}{` `}</>
)}
<Trans>
For <strong className="text-foreground">{min}</strong>{" "}
<Plural value={min} one=" minute" other=" minutes" />

View File

@@ -302,6 +302,13 @@ export default function SystemDetail({ name }: { name: string }) {
const hasGpuData = lastGpuVals.length > 0
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined)
let translatedStatus: string = system.status
if (system.status === "up") {
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
} else if (system.status === "down") {
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
}
return (
<>
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
@@ -328,7 +335,7 @@ export default function SystemDetail({ name }: { name: string }) {
})}
></span>
</span>
{system.status}
{translatedStatus}
</div>
{systemInfo.map(({ value, label, Icon, hide }, i) => {
if (hide || !value) {

View File

@@ -302,7 +302,8 @@ export const alertInfo: Record<string, AlertInfo> = {
unit: "",
icon: ServerIcon,
desc: () => t`Triggers when status switches between up and down`,
single: true,
/** "for x minutes" is appended to desc when only one value */
singleDesc: () => t`System` + " " + t`Down`,
},
CPU: {
name: () => t`CPU Usage`,

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

View File

@@ -0,0 +1,967 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2024-11-01 11:30-0400\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: el\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-07-13 02:50\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: el\n"
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 16\n"
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# day} other {# days}}"
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr ""
#: src/lib/utils.ts
msgid "1 week"
msgstr ""
#: src/lib/utils.ts
msgid "12 hours"
msgstr ""
#: src/lib/utils.ts
msgid "24 hours"
msgstr ""
#: src/lib/utils.ts
msgid "30 days"
msgstr ""
#. Table column
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions"
msgstr ""
#: src/components/routes/home.tsx
msgid "Active Alerts"
msgstr ""
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Προσθήκη <0>System</0>"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr ""
#: src/components/add-system.tsx
msgid "Add system"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Add URL"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Adjust display options for charts."
msgstr ""
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Agent"
msgstr ""
#: src/components/alerts/alert-button.tsx
#: src/components/alerts/alert-button.tsx
msgid "Alerts"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Are you sure you want to delete {name}?"
msgstr ""
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
msgstr ""
#: src/components/routes/system.tsx
msgid "Average"
msgstr ""
#: src/components/routes/system.tsx
msgid "Average CPU utilization of containers"
msgstr ""
#. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>"
msgstr ""
#: src/components/routes/system.tsx
msgid "Average power consumption of GPUs"
msgstr ""
#: src/components/routes/system.tsx
msgid "Average system-wide CPU utilization"
msgstr ""
#. placeholder {0}: gpu.n
#: src/components/routes/system.tsx
msgid "Average utilization of {0}"
msgstr ""
#: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups"
msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx
msgid "Bandwidth"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
msgstr ""
#: src/components/add-system.tsx
msgid "Binary"
msgstr ""
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Cancel"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Chart options"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link."
msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Check logs for more details."
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
msgid "Command line instructions"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Configure how you receive alert notifications."
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/auth-form.tsx
msgid "Confirm password"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Continue"
msgstr ""
#: src/lib/utils.ts
msgid "Copied to clipboard"
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker compose file content"
msgid "Copy docker compose"
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker run command"
msgid "Copy docker run"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Copy host"
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy Linux command"
msgstr ""
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr ""
#: src/components/add-system.tsx
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
msgstr ""
#: src/components/add-system.tsx
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "CPU"
msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx
#: src/components/charts/area-chart.tsx
msgid "CPU Usage"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr ""
#. Dark theme
#: src/components/mode-toggle.tsx
msgid "Dark"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
msgid "Dashboard"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Delete"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Disk"
msgstr ""
#: src/components/routes/system.tsx
msgid "Disk I/O"
msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx
#: src/components/charts/disk-chart.tsx
msgid "Disk Usage"
msgstr ""
#: src/components/routes/system.tsx
msgid "Disk usage of {extraFsName}"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker CPU Usage"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Network I/O"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr ""
#. Context: System is down
#: src/lib/utils.ts
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr ""
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Edit"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Email notifications"
msgstr ""
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Enter email address..."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/login/auth-form.tsx
msgid "Error"
msgstr ""
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
#: src/components/routes/home.tsx
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "Export your current systems configuration."
msgstr ""
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr ""
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr ""
#: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Filter..."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
#: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Forgot password?"
msgstr ""
#. Context: General settings
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General"
msgstr ""
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Grid"
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Homebrew command"
msgstr ""
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr ""
#. Linux kernel
#: src/components/routes/system.tsx
msgid "Kernel"
msgstr ""
#. Load average 15 minutes
#: src/components/systems-table/systems-table.tsx
msgid "L15"
msgstr ""
#. Load average 5 minutes
#: src/components/systems-table/systems-table.tsx
msgid "L5"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Layout"
msgstr ""
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr ""
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr ""
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr ""
#: src/components/navbar.tsx
msgid "Log Out"
msgstr ""
#: src/components/login/login.tsx
msgid "Login"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed"
msgstr ""
#: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Manage display and notification preferences."
msgstr ""
#: src/components/add-system.tsx
msgid "Manual setup instructions"
msgstr ""
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Memory"
msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx
msgid "Memory Usage"
msgstr ""
#: src/components/routes/system.tsx
msgid "Memory usage of docker containers"
msgstr ""
#: src/components/add-system.tsx
msgid "Name"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Net"
msgstr ""
#: src/components/routes/system.tsx
msgid "Network traffic of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Network traffic of public interfaces"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "No systems found."
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "OAuth 2 / OIDC support"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr ""
#: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts"
msgstr ""
#: src/components/command-palette.tsx
msgid "Page"
msgstr ""
#: src/components/command-palette.tsx
msgid "Pages / Settings"
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/auth-form.tsx
msgid "Password"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Password must be at least 8 characters."
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes."
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Pause"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr ""
#: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details."
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again"
msgstr ""
#: src/components/login/login.tsx
msgid "Please create an admin account"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Please enable pop-ups for this site"
msgstr ""
#: src/lib/utils.ts
msgid "Please log in again"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Please see <0>the documentation</0> for instructions."
msgstr ""
#: src/components/login/login.tsx
msgid "Please sign in to your account"
msgstr ""
#: src/components/add-system.tsx
msgid "Port"
msgstr ""
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Preferred Language"
msgstr ""
#. Use 'Key' if your language requires many more characters
#: src/components/add-system.tsx
msgid "Public Key"
msgstr ""
#. Disk read
#: src/components/charts/area-chart.tsx
#: src/components/charts/area-chart.tsx
msgid "Read"
msgstr ""
#. Network bytes received (download)
#: src/components/charts/area-chart.tsx
msgid "Received"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Resume"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr ""
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings"
msgstr ""
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
#: src/components/navbar.tsx
msgid "Search"
msgstr ""
#: src/components/command-palette.tsx
msgid "Search for systems or settings..."
msgstr ""
#: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr ""
#. Network bytes sent (upload)
#: src/components/charts/area-chart.tsx
msgid "Sent"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr ""
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Settings saved"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Sign in"
msgstr ""
#: src/components/command-palette.tsx
msgid "SMTP settings"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Sort By"
msgstr ""
#: src/lib/utils.ts
msgid "Status"
msgstr ""
#: src/components/routes/system.tsx
msgid "Swap space used by the system"
msgstr ""
#: src/components/routes/system.tsx
msgid "Swap Usage"
msgstr ""
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Table"
msgstr ""
#. Temperature label in systems table
#: src/components/systems-table/systems-table.tsx
msgid "Temp"
msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx
msgid "Temperature"
msgstr ""
#: src/components/routes/system.tsx
msgid "Temperatures of system sensors"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
msgstr ""
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
msgstr ""
#: src/components/routes/system.tsx
msgid "Throughput of root filesystem"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr ""
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Toggle grid"
msgstr ""
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when status switches between up and down"
msgstr ""
#: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
#. Context: System is up
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."
msgstr ""
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr ""
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
#: src/components/charts/area-chart.tsx
msgid "Usage"
msgstr ""
#: src/components/routes/system.tsx
msgid "Usage of root partition"
msgstr ""
#: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
#: src/components/charts/area-chart.tsx
msgid "Used"
msgstr ""
#: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Webhook / Push notifications"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Windows command"
msgstr ""
#. Disk write
#: src/components/charts/area-chart.tsx
#: src/components/charts/area-chart.tsx
msgid "Write"
msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "YAML Config"
msgstr ""
#: src/components/routes/settings/config-yaml.tsx
msgid "YAML Configuration"
msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr ""

View File

@@ -103,27 +103,27 @@ msgstr "Are you sure you want to delete {name}?"
msgid "Automatic copy requires a secure context."
msgstr "Automatic copy requires a secure context."
#: src/components/routes/system.tsx:626
#: src/components/routes/system.tsx:633
msgid "Average"
msgstr "Average"
#: src/components/routes/system.tsx:403
#: src/components/routes/system.tsx:410
msgid "Average CPU utilization of containers"
msgstr "Average CPU utilization of containers"
#: src/components/alerts/alerts-system.tsx:206
#: src/components/alerts/alerts-system.tsx:205
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Average exceeds <0>{value}{0}</0>"
#: src/components/routes/system.tsx:504
#: src/components/routes/system.tsx:511
msgid "Average power consumption of GPUs"
msgstr "Average power consumption of GPUs"
#: src/components/routes/system.tsx:392
#: src/components/routes/system.tsx:399
msgid "Average system-wide CPU utilization"
msgstr "Average system-wide CPU utilization"
#: src/components/routes/system.tsx:522
#: src/components/routes/system.tsx:529
msgid "Average utilization of {0}"
msgstr "Average utilization of {0}"
@@ -132,8 +132,8 @@ msgstr "Average utilization of {0}"
msgid "Backups"
msgstr "Backups"
#: src/components/routes/system.tsx:448
#: src/lib/utils.ts:326
#: src/components/routes/system.tsx:455
#: src/lib/utils.ts:327
msgid "Bandwidth"
msgstr "Bandwidth"
@@ -229,8 +229,8 @@ msgid "CPU"
msgstr "CPU"
#: src/components/charts/area-chart.tsx:56
#: src/components/routes/system.tsx:391
#: src/lib/utils.ts:308
#: src/components/routes/system.tsx:398
#: src/lib/utils.ts:309
msgid "CPU Usage"
msgstr "CPU Usage"
@@ -260,29 +260,29 @@ msgstr "Delete"
msgid "Disk"
msgstr "Disk"
#: src/components/routes/system.tsx:438
#: src/components/routes/system.tsx:445
msgid "Disk I/O"
msgstr "Disk I/O"
#: src/components/charts/disk-chart.tsx:79
#: src/components/routes/system.tsx:431
#: src/lib/utils.ts:320
#: src/components/routes/system.tsx:438
#: src/lib/utils.ts:321
msgid "Disk Usage"
msgstr "Disk Usage"
#: src/components/routes/system.tsx:559
#: src/components/routes/system.tsx:566
msgid "Disk usage of {extraFsName}"
msgstr "Disk usage of {extraFsName}"
#: src/components/routes/system.tsx:402
#: src/components/routes/system.tsx:409
msgid "Docker CPU Usage"
msgstr "Docker CPU Usage"
#: src/components/routes/system.tsx:423
#: src/components/routes/system.tsx:430
msgid "Docker Memory Usage"
msgstr "Docker Memory Usage"
#: src/components/routes/system.tsx:464
#: src/components/routes/system.tsx:471
msgid "Docker Network I/O"
msgstr "Docker Network I/O"
@@ -290,6 +290,12 @@ msgstr "Docker Network I/O"
msgid "Documentation"
msgstr "Documentation"
#. Context: System is down
#: src/components/routes/system.tsx:309
#: src/lib/utils.ts:306
msgid "Down"
msgstr "Down"
#: src/components/add-system.tsx:124
#: src/components/systems-table/systems-table.tsx:599
msgid "Edit"
@@ -351,12 +357,12 @@ msgstr "Failed to send test notification"
msgid "Failed to update alert"
msgstr "Failed to update alert"
#: src/components/routes/system.tsx:599
#: src/components/routes/system.tsx:606
#: src/components/systems-table/systems-table.tsx:326
msgid "Filter..."
msgstr "Filter..."
#: src/components/alerts/alerts-system.tsx:227
#: src/components/alerts/alerts-system.tsx:230
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -370,7 +376,7 @@ msgstr "Forgot password?"
msgid "General"
msgstr "General"
#: src/components/routes/system.tsx:503
#: src/components/routes/system.tsx:510
msgid "GPU Power Draw"
msgstr "GPU Power Draw"
@@ -439,7 +445,7 @@ msgid "Manual setup instructions"
msgstr "Manual setup instructions"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx:629
#: src/components/routes/system.tsx:636
msgid "Max 1 min"
msgstr "Max 1 min"
@@ -447,12 +453,12 @@ msgstr "Max 1 min"
msgid "Memory"
msgstr "Memory"
#: src/components/routes/system.tsx:413
#: src/lib/utils.ts:314
#: src/components/routes/system.tsx:420
#: src/lib/utils.ts:315
msgid "Memory Usage"
msgstr "Memory Usage"
#: src/components/routes/system.tsx:424
#: src/components/routes/system.tsx:431
msgid "Memory usage of docker containers"
msgstr "Memory usage of docker containers"
@@ -464,11 +470,11 @@ msgstr "Name"
msgid "Net"
msgstr "Net"
#: src/components/routes/system.tsx:465
#: src/components/routes/system.tsx:472
msgid "Network traffic of docker containers"
msgstr "Network traffic of docker containers"
#: src/components/routes/system.tsx:450
#: src/components/routes/system.tsx:457
msgid "Network traffic of public interfaces"
msgstr "Network traffic of public interfaces"
@@ -573,8 +579,8 @@ msgstr "Please sign in to your account"
msgid "Port"
msgstr "Port"
#: src/components/routes/system.tsx:414
#: src/components/routes/system.tsx:530
#: src/components/routes/system.tsx:421
#: src/components/routes/system.tsx:537
msgid "Precise utilization at the recorded time"
msgstr "Precise utilization at the recorded time"
@@ -668,11 +674,11 @@ msgstr "Sort By"
msgid "Status"
msgstr "Status"
#: src/components/routes/system.tsx:480
#: src/components/routes/system.tsx:487
msgid "Swap space used by the system"
msgstr "Swap space used by the system"
#: src/components/routes/system.tsx:479
#: src/components/routes/system.tsx:486
msgid "Swap Usage"
msgstr "Swap Usage"
@@ -682,6 +688,7 @@ msgstr "Swap Usage"
#: src/components/systems-table/systems-table.tsx:133
#: src/components/systems-table/systems-table.tsx:144
#: src/components/systems-table/systems-table.tsx:518
#: src/lib/utils.ts:306
msgid "System"
msgstr "System"
@@ -702,12 +709,12 @@ msgstr "Table"
msgid "Temp"
msgstr "Temp"
#: src/components/routes/system.tsx:491
#: src/lib/utils.ts:333
#: src/components/routes/system.tsx:498
#: src/lib/utils.ts:334
msgid "Temperature"
msgstr "Temperature"
#: src/components/routes/system.tsx:492
#: src/components/routes/system.tsx:499
msgid "Temperatures of system sensors"
msgstr "Temperatures of system sensors"
@@ -735,11 +742,11 @@ msgstr "Then log into the backend and reset your user account password in the us
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
msgstr "This action cannot be undone. This will permanently delete all current records for {name} from the database."
#: src/components/routes/system.tsx:571
#: src/components/routes/system.tsx:578
msgid "Throughput of {extraFsName}"
msgstr "Throughput of {extraFsName}"
#: src/components/routes/system.tsx:439
#: src/components/routes/system.tsx:446
msgid "Throughput of root filesystem"
msgstr "Throughput of root filesystem"
@@ -747,8 +754,8 @@ msgstr "Throughput of root filesystem"
msgid "To email(s)"
msgstr "To email(s)"
#: src/components/routes/system.tsx:366
#: src/components/routes/system.tsx:379
#: src/components/routes/system.tsx:373
#: src/components/routes/system.tsx:386
msgid "Toggle grid"
msgstr "Toggle grid"
@@ -756,19 +763,19 @@ msgstr "Toggle grid"
msgid "Toggle theme"
msgstr "Toggle theme"
#: src/lib/utils.ts:336
#: src/lib/utils.ts:337
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Triggers when any sensor exceeds a threshold"
#: src/lib/utils.ts:329
#: src/lib/utils.ts:330
msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Triggers when combined up/down exceeds a threshold"
#: src/lib/utils.ts:311
#: src/lib/utils.ts:312
msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Triggers when CPU usage exceeds a threshold"
#: src/lib/utils.ts:317
#: src/lib/utils.ts:318
msgid "Triggers when memory usage exceeds a threshold"
msgstr "Triggers when memory usage exceeds a threshold"
@@ -776,10 +783,15 @@ msgstr "Triggers when memory usage exceeds a threshold"
msgid "Triggers when status switches between up and down"
msgstr "Triggers when status switches between up and down"
#: src/lib/utils.ts:323
#: src/lib/utils.ts:324
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Triggers when usage of any disk exceeds a threshold"
#. Context: System is up
#: src/components/routes/system.tsx:307
msgid "Up"
msgstr "Up"
#: src/components/systems-table/systems-table.tsx:322
msgid "Updated in real time. Click on a system to view information."
msgstr "Updated in real time. Click on a system to view information."
@@ -789,12 +801,12 @@ msgid "Uptime"
msgstr "Uptime"
#: src/components/charts/area-chart.tsx:73
#: src/components/routes/system.tsx:521
#: src/components/routes/system.tsx:558
#: src/components/routes/system.tsx:528
#: src/components/routes/system.tsx:565
msgid "Usage"
msgstr "Usage"
#: src/components/routes/system.tsx:431
#: src/components/routes/system.tsx:438
msgid "Usage of root partition"
msgstr "Usage of root partition"
@@ -817,7 +829,7 @@ msgstr "View"
msgid "Visible Fields"
msgstr "Visible Fields"
#: src/components/routes/system.tsx:663
#: src/components/routes/system.tsx:670
msgid "Waiting for enough records to display"
msgstr "Waiting for enough records to display"

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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,7 @@ interface AlertInfo {
unit: string
icon: any
desc: () => string
single?: boolean
max?: number
/** Single value description (when there's only one value, like status) */
singleDesc?: () => string
}