Compare commits

...

522 Commits

Author SHA1 Message Date
hank
d433962a41 New translations en.po (Indonesian) 2025-12-25 05:45:43 -05:00
hank
c1d94ac29f New translations en.po (Indonesian) 2025-12-25 04:22:51 -05:00
hank
9f4e56c9cf New translations en.po (Romanian) 2025-12-19 06:58:15 -05:00
hank
a3ebfbf37f New translations en.po (Romanian) 2025-12-19 05:55:07 -05:00
hank
cb90e96ae5 New translations en.po (Danish) 2025-12-19 05:55:06 -05:00
hank
25ed5d0d66 New translations en.po (Danish) 2025-12-19 03:46:19 -05:00
hank
3e5b91056e New translations en.po (Polish) 2025-12-18 14:21:36 -05:00
hank
dc79f24c06 New translations en.po (Thai) 2025-12-17 13:16:58 -05:00
hank
f9f5258b22 New translations en.po (Dutch) 2025-12-17 07:02:37 -05:00
hank
0b611cda57 New translations en.po (Dutch) 2025-12-17 05:18:18 -05:00
hank
74635e5763 New translations en.po (Spanish) 2025-12-14 04:39:12 -05:00
hank
8ce9088d9d New translations en.po (Serbian (Cyrillic)) 2025-12-08 13:22:54 -05:00
hank
b3331c00f8 New translations en.po (Chinese Traditional) 2025-12-08 10:28:57 -05:00
hank
d0559065c1 New translations en.po (Chinese Traditional) 2025-12-08 08:54:57 -05:00
hank
78f6006bdc New translations en.po (Serbian (Cyrillic)) 2025-12-05 15:24:47 -05:00
hank
436b42f4d1 New translations en.po (Swedish) 2025-12-05 15:24:42 -05:00
hank
16f7b30624 New translations en.po (Slovenian) 2025-12-05 15:24:41 -05:00
hank
9249256c9f New translations en.po (Portuguese) 2025-12-05 15:24:40 -05:00
hank
34163b8595 New translations en.po (Norwegian) 2025-12-05 15:24:38 -05:00
hank
a4731f9179 New translations en.po (German) 2025-12-05 15:24:34 -05:00
hank
9c74eccaf0 New translations en.po (Danish) 2025-12-05 15:24:33 -05:00
hank
4768adf440 New translations en.po (Czech) 2025-12-05 15:24:32 -05:00
hank
11ffb422e8 New translations en.po (French) 2025-12-05 15:24:29 -05:00
hank
bab02ad738 New translations en.po (Spanish) 2025-12-05 15:24:28 -05:00
hank
61faee2450 New translations en.po (Serbian (Cyrillic)) 2025-12-04 15:03:35 -05:00
hank
d0be54f47c New translations en.po (Chinese Traditional, Hong Kong) 2025-12-02 18:18:21 -05:00
hank
cc2be97055 New translations en.po (Croatian) 2025-12-02 18:18:20 -05:00
hank
2fc1565b75 New translations en.po (Persian) 2025-12-02 18:18:19 -05:00
hank
f5421eff3c New translations en.po (Indonesian) 2025-12-02 18:18:18 -05:00
hank
fc01ca1cad New translations en.po (Vietnamese) 2025-12-02 18:18:17 -05:00
hank
e2923126d2 New translations en.po (Chinese Simplified) 2025-12-02 18:18:16 -05:00
hank
972ce62ff5 New translations en.po (Ukrainian) 2025-12-02 18:18:15 -05:00
hank
05d9297ca3 New translations en.po (Turkish) 2025-12-02 18:18:14 -05:00
hank
ffc35b3c51 New translations en.po (Swedish) 2025-12-02 18:18:13 -05:00
hank
a547de8bf0 New translations en.po (Slovenian) 2025-12-02 18:18:12 -05:00
hank
d8d1a89256 New translations en.po (Portuguese) 2025-12-02 18:18:11 -05:00
hank
c29ba1c353 New translations en.po (Polish) 2025-12-02 18:18:10 -05:00
hank
474d860929 New translations en.po (Norwegian) 2025-12-02 18:18:09 -05:00
hank
fb57a57e77 New translations en.po (Dutch) 2025-12-02 18:18:08 -05:00
hank
02e5a8e9fc New translations en.po (Korean) 2025-12-02 18:18:07 -05:00
hank
c0f6f64aa7 New translations en.po (Japanese) 2025-12-02 18:18:06 -05:00
hank
3077ed045d New translations en.po (Hungarian) 2025-12-02 18:18:05 -05:00
hank
f7b62a2868 New translations en.po (Hebrew) 2025-12-02 18:18:04 -05:00
hank
5daa0d3576 New translations en.po (German) 2025-12-02 18:18:03 -05:00
hank
a6b9fa2aa9 New translations en.po (Danish) 2025-12-02 18:18:02 -05:00
hank
5195e6d675 New translations en.po (Czech) 2025-12-02 18:18:01 -05:00
hank
93e71dcf30 New translations en.po (Bulgarian) 2025-12-02 18:18:00 -05:00
hank
826227f3af New translations en.po (Arabic) 2025-12-02 18:17:59 -05:00
hank
cfe8645c18 New translations en.po (French) 2025-12-02 18:17:58 -05:00
hank
681184b444 New translations en.po (Romanian) 2025-12-02 18:17:56 -05:00
hank
6d759cbe9f New translations en.po (Spanish) 2025-12-02 18:17:55 -05:00
hank
75480f66fa New translations en.po (Russian) 2025-12-02 18:17:55 -05:00
hank
416c237ef3 New translations en.po (Italian) 2025-12-02 18:17:53 -05:00
hank
ff64ac7a37 New translations en.po (Chinese Traditional) 2025-12-02 18:17:52 -05:00
hank
555f668b54 New translations en.po (Spanish) 2025-12-01 18:32:58 -05:00
hank
0dedc634a7 New translations en.po (Russian) 2025-12-01 18:32:57 -05:00
hank
5500e45951 New translations en.po (Chinese Traditional) 2025-12-01 18:32:56 -05:00
hank
e7574a927f New translations en.po (Chinese Traditional) 2025-11-22 06:14:37 -05:00
hank
83fbaa7a3f New translations en.po (Chinese Traditional) 2025-11-22 00:56:30 -05:00
hank
cf3efa1f9f New translations en.po (Chinese Traditional) 2025-11-21 22:50:27 -05:00
hank
24093e33a9 New translations en.po (Russian) 2025-11-21 18:15:30 -05:00
hank
075fad1da4 New translations en.po (Italian) 2025-11-20 11:58:14 -05:00
hank
a35631415a New translations en.po (Polish) 2025-11-17 11:53:50 -05:00
hank
8e99d67174 New translations en.po (Spanish) 2025-11-15 03:26:28 -05:00
hank
cf37c9a93c New translations en.po (Indonesian) 2025-11-14 17:51:43 -05:00
hank
402d1d9fec New translations en.po (Hebrew) 2025-11-14 17:51:41 -05:00
hank
b4f2afa4b6 New translations en.po (Chinese Traditional, Hong Kong) 2025-11-14 17:51:40 -05:00
hank
beff2eb43f New translations en.po (Persian) 2025-11-14 17:51:39 -05:00
hank
2e0d12a02d New translations en.po (Vietnamese) 2025-11-14 17:51:38 -05:00
hank
bc2fd34ac5 New translations en.po (Chinese Simplified) 2025-11-14 17:51:37 -05:00
hank
333cfae109 New translations en.po (Ukrainian) 2025-11-14 17:51:36 -05:00
hank
8cf8dd492d New translations en.po (Turkish) 2025-11-14 17:51:35 -05:00
hank
9b664b6400 New translations en.po (Swedish) 2025-11-14 17:51:34 -05:00
hank
c30d2fbe39 New translations en.po (Slovenian) 2025-11-14 17:51:33 -05:00
hank
f1a4fae659 New translations en.po (Russian) 2025-11-14 17:51:32 -05:00
hank
32675e403f New translations en.po (Polish) 2025-11-14 17:51:31 -05:00
hank
d92d67eece New translations en.po (Korean) 2025-11-14 17:51:30 -05:00
hank
ed1fc63ce2 New translations en.po (Japanese) 2025-11-14 17:51:29 -05:00
hank
0e2b1675fa New translations en.po (Italian) 2025-11-14 17:51:28 -05:00
hank
ee8f901918 New translations en.po (Danish) 2025-11-14 17:51:27 -05:00
hank
7dc2a86b1e New translations en.po (Czech) 2025-11-14 17:51:26 -05:00
hank
51699ddc12 New translations en.po (Bulgarian) 2025-11-14 17:51:25 -05:00
hank
e0675567b8 New translations en.po (Arabic) 2025-11-14 17:51:24 -05:00
hank
61b3102eda New translations en.po (Spanish) 2025-11-14 17:51:23 -05:00
hank
7cc8f2b933 New translations en.po (Romanian) 2025-11-14 17:51:22 -05:00
hank
d38e3eab9c New translations en.po (Croatian) 2025-11-14 17:51:21 -05:00
hank
4ceb06b0c5 New translations en.po (Chinese Traditional) 2025-11-14 17:51:19 -05:00
hank
b1a468a0ab New translations en.po (Portuguese) 2025-11-14 17:51:18 -05:00
hank
c6755183a8 New translations en.po (Norwegian) 2025-11-14 17:51:17 -05:00
hank
cdc9f11ac0 New translations en.po (Dutch) 2025-11-14 17:51:16 -05:00
hank
6ccaaee57e New translations en.po (Hungarian) 2025-11-14 17:51:15 -05:00
hank
214ee4a75a New translations en.po (German) 2025-11-14 17:51:14 -05:00
hank
a506d3c84a New translations en.po (French) 2025-11-14 17:51:13 -05:00
hank
812e849769 New translations en.po (Spanish) 2025-11-14 12:09:18 -05:00
hank
774eef1f3f New translations en.po (Polish) 2025-11-13 09:52:30 -05:00
hank
fc85c50f2f New translations en.po (Indonesian) 2025-11-12 15:36:57 -05:00
hank
fad22eee61 New translations en.po (Hebrew) 2025-11-12 15:36:56 -05:00
hank
e84dbd639b New translations en.po (Chinese Traditional, Hong Kong) 2025-11-12 15:36:55 -05:00
hank
2dd59b4e11 New translations en.po (Persian) 2025-11-12 15:36:54 -05:00
hank
bb649971dc New translations en.po (Vietnamese) 2025-11-12 15:36:53 -05:00
hank
3f82ee0330 New translations en.po (Chinese Simplified) 2025-11-12 15:36:52 -05:00
hank
a26fde66e6 New translations en.po (Ukrainian) 2025-11-12 15:36:51 -05:00
hank
ed9b576bde New translations en.po (Turkish) 2025-11-12 15:36:49 -05:00
hank
aea463f5da New translations en.po (Swedish) 2025-11-12 15:36:48 -05:00
hank
45e00c70ab New translations en.po (Slovenian) 2025-11-12 15:36:47 -05:00
hank
7fc4655f13 New translations en.po (Russian) 2025-11-12 15:36:46 -05:00
hank
7dcaeaa2b4 New translations en.po (Polish) 2025-11-12 15:36:45 -05:00
hank
d07293bf1a New translations en.po (Korean) 2025-11-12 15:36:44 -05:00
hank
38298bbeab New translations en.po (Japanese) 2025-11-12 15:36:43 -05:00
hank
0124ccfac1 New translations en.po (Italian) 2025-11-12 15:36:42 -05:00
hank
5f62ebcd7b New translations en.po (Danish) 2025-11-12 15:36:41 -05:00
hank
ac354e9d6b New translations en.po (Czech) 2025-11-12 15:36:40 -05:00
hank
5572cad7f6 New translations en.po (Bulgarian) 2025-11-12 15:36:39 -05:00
hank
5fa6a7c4e4 New translations en.po (Arabic) 2025-11-12 15:36:37 -05:00
hank
0f9442eaf8 New translations en.po (Spanish) 2025-11-12 15:36:36 -05:00
hank
6094d8d92d New translations en.po (Romanian) 2025-11-12 15:36:35 -05:00
hank
1469710166 New translations en.po (Croatian) 2025-11-12 15:36:34 -05:00
hank
c1df7edddc New translations en.po (Chinese Traditional) 2025-11-12 15:36:33 -05:00
hank
e70f06285c New translations en.po (Portuguese) 2025-11-12 15:36:32 -05:00
hank
c5efa9b20c New translations en.po (Norwegian) 2025-11-12 15:36:31 -05:00
hank
29a948ece4 New translations en.po (Dutch) 2025-11-12 15:36:30 -05:00
hank
d9b587c67b New translations en.po (Hungarian) 2025-11-12 15:36:28 -05:00
hank
f806ae58b6 New translations en.po (German) 2025-11-12 15:36:27 -05:00
hank
8b5dd8dedd New translations en.po (French) 2025-11-12 15:36:26 -05:00
hank
222e5addef New translations en.po (French) 2025-11-11 14:25:02 -05:00
hank
21e7bae720 New translations en.po (Romanian) 2025-11-10 20:21:58 -05:00
hank
a13d90d794 New translations en.po (Swedish) 2025-11-09 20:57:18 -05:00
hank
6e900d0f26 New translations en.po (Portuguese) 2025-11-08 18:43:12 -05:00
hank
112f1853ee New translations en.po (Swedish) 2025-11-06 10:06:27 -05:00
hank
7aa9e3a6d3 New translations en.po (Spanish) 2025-11-05 13:06:57 -05:00
hank
6e9bc6a53b New translations en.po (French) 2025-11-05 10:16:19 -05:00
hank
12eb884b1e New translations en.po (Indonesian) 2025-11-04 18:47:36 -05:00
hank
270afa1c00 New translations en.po (Hebrew) 2025-11-04 18:47:35 -05:00
hank
35505f2d50 New translations en.po (Chinese Traditional, Hong Kong) 2025-11-04 18:47:34 -05:00
hank
5a28ba3a74 New translations en.po (Persian) 2025-11-04 18:47:33 -05:00
hank
08d9126883 New translations en.po (Vietnamese) 2025-11-04 18:47:32 -05:00
hank
e9829229b6 New translations en.po (Chinese Simplified) 2025-11-04 18:47:31 -05:00
hank
6d7bb7ceee New translations en.po (Ukrainian) 2025-11-04 18:47:30 -05:00
hank
60a8b06b72 New translations en.po (Turkish) 2025-11-04 18:47:29 -05:00
hank
c4a145836b New translations en.po (Swedish) 2025-11-04 18:47:28 -05:00
hank
ea37451c98 New translations en.po (Slovenian) 2025-11-04 18:47:27 -05:00
hank
1faad84cc2 New translations en.po (Russian) 2025-11-04 18:47:26 -05:00
hank
d0b6200c5b New translations en.po (Polish) 2025-11-04 18:47:25 -05:00
hank
fb66760665 New translations en.po (Korean) 2025-11-04 18:47:24 -05:00
hank
480f1596bb New translations en.po (Japanese) 2025-11-04 18:47:23 -05:00
hank
6c11e2954e New translations en.po (Italian) 2025-11-04 18:47:22 -05:00
hank
6a5f6530ef New translations en.po (Danish) 2025-11-04 18:47:21 -05:00
hank
a4ee88bf7f New translations en.po (Czech) 2025-11-04 18:47:20 -05:00
hank
d9f1f06b15 New translations en.po (Bulgarian) 2025-11-04 18:47:19 -05:00
hank
4c00988a37 New translations en.po (Arabic) 2025-11-04 18:47:18 -05:00
hank
193fbe9d31 New translations en.po (Spanish) 2025-11-04 18:47:17 -05:00
hank
ca6b6394ef New translations en.po (Romanian) 2025-11-04 18:47:16 -05:00
hank
1600b94846 New translations en.po (Croatian) 2025-11-04 18:47:15 -05:00
hank
b5aa66224a New translations en.po (Chinese Traditional) 2025-11-04 18:47:15 -05:00
hank
5a14eafae5 New translations en.po (Portuguese) 2025-11-04 18:47:14 -05:00
hank
0b11dcdb1b New translations en.po (Norwegian) 2025-11-04 18:47:13 -05:00
hank
fc1c135e71 New translations en.po (Dutch) 2025-11-04 18:47:12 -05:00
hank
7cb6966335 New translations en.po (Hungarian) 2025-11-04 18:47:11 -05:00
hank
c090cf9e3e New translations en.po (German) 2025-11-04 18:47:10 -05:00
hank
224bce7616 New translations en.po (French) 2025-11-04 18:47:09 -05:00
hank
7fc3afb82b New translations en.po (Spanish) 2025-11-04 17:13:09 -05:00
hank
affdd66065 New translations en.po (Portuguese) 2025-11-04 17:13:06 -05:00
hank
466c5a237b New translations en.po (Portuguese) 2025-11-02 10:12:47 -05:00
hank
827758c97f New translations en.po (Spanish) 2025-11-02 01:32:54 -05:00
hank
fb4a35b054 New translations en.po (Indonesian) 2025-11-01 15:01:34 -04:00
hank
ba561ec34c New translations en.po (Hebrew) 2025-11-01 15:01:33 -04:00
hank
a94f85794c New translations en.po (Chinese Traditional, Hong Kong) 2025-11-01 15:01:32 -04:00
hank
aa408f82c6 New translations en.po (Persian) 2025-11-01 15:01:31 -04:00
hank
e8045a3438 New translations en.po (Vietnamese) 2025-11-01 15:01:30 -04:00
hank
7afd678f54 New translations en.po (Chinese Simplified) 2025-11-01 15:01:28 -04:00
hank
77e2b98470 New translations en.po (Ukrainian) 2025-11-01 15:01:27 -04:00
hank
70e894caf4 New translations en.po (Turkish) 2025-11-01 15:01:26 -04:00
hank
433bd6dde1 New translations en.po (Swedish) 2025-11-01 15:01:25 -04:00
hank
f60ee6a839 New translations en.po (Slovenian) 2025-11-01 15:01:24 -04:00
hank
56e6dbf0a8 New translations en.po (Russian) 2025-11-01 15:01:23 -04:00
hank
7ae179764d New translations en.po (Polish) 2025-11-01 15:01:22 -04:00
hank
e54cac2c7b New translations en.po (Korean) 2025-11-01 15:01:21 -04:00
hank
6bc8878408 New translations en.po (Japanese) 2025-11-01 15:01:20 -04:00
hank
0f6d85f124 New translations en.po (Italian) 2025-11-01 15:01:19 -04:00
hank
b6717c11ae New translations en.po (Danish) 2025-11-01 15:01:18 -04:00
hank
3cff9ccff8 New translations en.po (Czech) 2025-11-01 15:01:17 -04:00
hank
3fb5f065b8 New translations en.po (Bulgarian) 2025-11-01 15:01:16 -04:00
hank
73e397abbf New translations en.po (Arabic) 2025-11-01 15:01:15 -04:00
hank
38f63b02bd New translations en.po (Spanish) 2025-11-01 15:01:14 -04:00
hank
879e84bb34 New translations en.po (Romanian) 2025-11-01 15:01:13 -04:00
hank
79b709d53c New translations en.po (Croatian) 2025-11-01 15:01:12 -04:00
hank
97e188a619 New translations en.po (Chinese Traditional) 2025-11-01 15:01:11 -04:00
hank
9459b59b14 New translations en.po (Portuguese) 2025-11-01 15:01:10 -04:00
hank
c281a0717f New translations en.po (Norwegian) 2025-11-01 15:01:09 -04:00
hank
b274261d3e New translations en.po (Dutch) 2025-11-01 15:01:08 -04:00
hank
63a78f2829 New translations en.po (Hungarian) 2025-11-01 15:01:07 -04:00
hank
31aa6df5b2 New translations en.po (German) 2025-11-01 15:01:06 -04:00
hank
74d8f685bd New translations en.po (French) 2025-11-01 15:01:05 -04:00
hank
43a32ef0ce New translations en.po (Spanish) 2025-11-01 13:41:18 -04:00
hank
4426b53f47 New translations en.po (Chinese Traditional) 2025-11-01 03:52:31 -04:00
hank
6902e90ca7 New translations en.po (Hebrew) 2025-10-30 17:53:10 -04:00
hank
9aeb80ce88 New translations en.po (Chinese Simplified) 2025-10-30 17:53:08 -04:00
hank
60c557055f New translations en.po (Italian) 2025-10-30 17:53:02 -04:00
hank
0a6020b6b8 New translations en.po (Arabic) 2025-10-30 17:52:59 -04:00
hank
7f85b8f2a9 New translations en.po (Spanish) 2025-10-30 17:52:58 -04:00
hank
5ca388d855 New translations en.po (Portuguese) 2025-10-30 17:52:56 -04:00
hank
34d74b3bcd New translations en.po (Hebrew) 2025-10-30 16:47:52 -04:00
hank
86ff86aa99 New translations en.po (Indonesian) 2025-10-29 17:56:50 -04:00
hank
d9a463e465 New translations en.po (Hebrew) 2025-10-29 17:56:49 -04:00
hank
a5d0690a81 New translations en.po (Chinese Traditional, Hong Kong) 2025-10-28 19:00:28 -04:00
hank
eef4092b16 New translations en.po (Persian) 2025-10-28 19:00:27 -04:00
hank
a4e89676df New translations en.po (Vietnamese) 2025-10-28 19:00:26 -04:00
hank
3f083686fe New translations en.po (Chinese Simplified) 2025-10-28 19:00:25 -04:00
hank
cf2ccc1eb4 New translations en.po (Ukrainian) 2025-10-28 19:00:24 -04:00
hank
43c44a085b New translations en.po (Turkish) 2025-10-28 19:00:23 -04:00
hank
e90dfde12e New translations en.po (Swedish) 2025-10-28 19:00:22 -04:00
hank
a1bb77f4f2 New translations en.po (Slovenian) 2025-10-28 19:00:21 -04:00
hank
ef582ec1ef New translations en.po (Russian) 2025-10-28 19:00:20 -04:00
hank
2c30bdb2e4 New translations en.po (Polish) 2025-10-28 19:00:19 -04:00
hank
5bfbc0420a New translations en.po (Korean) 2025-10-28 19:00:18 -04:00
hank
b7a3222d31 New translations en.po (Japanese) 2025-10-28 19:00:17 -04:00
hank
d4955af3ba New translations en.po (Italian) 2025-10-28 19:00:16 -04:00
hank
bde88dda26 New translations en.po (Danish) 2025-10-28 19:00:14 -04:00
hank
3f51bd5ec6 New translations en.po (Czech) 2025-10-28 19:00:13 -04:00
hank
092e8c9948 New translations en.po (Bulgarian) 2025-10-28 19:00:11 -04:00
hank
c35762de98 New translations en.po (Arabic) 2025-10-28 19:00:10 -04:00
hank
7c43e9e27c New translations en.po (Spanish) 2025-10-28 19:00:09 -04:00
hank
ee07a0d181 New translations en.po (Romanian) 2025-10-28 19:00:06 -04:00
hank
3178587f20 New translations en.po (Croatian) 2025-10-28 19:00:05 -04:00
hank
b07a791d6a New translations en.po (Chinese Traditional) 2025-10-28 19:00:03 -04:00
hank
8b6918b4a5 New translations en.po (Portuguese) 2025-10-28 19:00:02 -04:00
hank
c7733146b7 New translations en.po (Norwegian) 2025-10-28 19:00:01 -04:00
hank
8f23bbd436 New translations en.po (Dutch) 2025-10-28 18:59:59 -04:00
hank
7ca3cca15d New translations en.po (Hungarian) 2025-10-28 18:59:58 -04:00
hank
dd553ec7b6 New translations en.po (German) 2025-10-28 18:59:57 -04:00
hank
1a8dd0ab32 New translations en.po (French) 2025-10-28 18:59:56 -04:00
hank
48e0c1efbf New translations en.po (German) 2025-10-27 19:49:39 -04:00
hank
8cdddc9f5e New translations en.po (Spanish) 2025-10-27 14:01:40 -04:00
hank
756c5eab3e New translations en.po (Italian) 2025-10-26 22:07:18 -04:00
hank
cf8102d547 New translations en.po (Danish) 2025-10-26 15:21:12 -04:00
hank
156a54f26c New translations en.po (Polish) 2025-10-26 05:04:19 -04:00
hank
ad1e7772af New translations en.po (Chinese Traditional, Hong Kong) 2025-10-25 19:16:37 -04:00
hank
62edf55a37 New translations en.po (Persian) 2025-10-25 19:16:36 -04:00
hank
223b627619 New translations en.po (Vietnamese) 2025-10-25 19:16:35 -04:00
hank
d3bc1a6764 New translations en.po (Chinese Simplified) 2025-10-25 19:16:34 -04:00
hank
5157144504 New translations en.po (Ukrainian) 2025-10-25 19:16:33 -04:00
hank
cdb396408f New translations en.po (Turkish) 2025-10-25 19:16:32 -04:00
hank
ca7c68140a New translations en.po (Swedish) 2025-10-25 19:16:31 -04:00
hank
5af7afb970 New translations en.po (Slovenian) 2025-10-25 19:16:30 -04:00
hank
fd053fc8e5 New translations en.po (Russian) 2025-10-25 19:16:29 -04:00
hank
f1f01657c0 New translations en.po (Polish) 2025-10-25 19:16:28 -04:00
hank
1c9f03c848 New translations en.po (Korean) 2025-10-25 19:16:27 -04:00
hank
82b5ee0424 New translations en.po (Japanese) 2025-10-25 19:16:26 -04:00
hank
0885bf2ba4 New translations en.po (Italian) 2025-10-25 19:16:25 -04:00
hank
d541b42bef New translations en.po (Danish) 2025-10-25 19:16:24 -04:00
hank
29599cd59c New translations en.po (Czech) 2025-10-25 19:16:23 -04:00
hank
5fff9bd3ac New translations en.po (Bulgarian) 2025-10-25 19:16:22 -04:00
hank
81b6198ee7 New translations en.po (Arabic) 2025-10-25 19:16:21 -04:00
hank
01f58a328e New translations en.po (Spanish) 2025-10-25 19:16:20 -04:00
hank
6ffef3c33b New translations en.po (Romanian) 2025-10-25 19:16:19 -04:00
hank
3d50d0cbba New translations en.po (Croatian) 2025-10-25 19:16:18 -04:00
hank
a8ec54f5a5 New translations en.po (Chinese Traditional) 2025-10-25 19:16:17 -04:00
hank
3c44f51671 New translations en.po (Portuguese) 2025-10-25 19:16:16 -04:00
hank
449642fdd2 New translations en.po (Norwegian) 2025-10-25 19:16:15 -04:00
hank
bb324258d6 New translations en.po (Dutch) 2025-10-25 19:16:14 -04:00
hank
693117724a New translations en.po (Hungarian) 2025-10-25 19:16:13 -04:00
hank
008dd9d184 New translations en.po (German) 2025-10-25 19:16:12 -04:00
hank
f171ec9932 New translations en.po (French) 2025-10-25 19:16:11 -04:00
hank
9e6e1771d1 New translations en.po (Chinese Simplified) 2025-10-25 17:09:25 -04:00
hank
bde6264d11 New translations en.po (Spanish) 2025-10-25 17:09:17 -04:00
hank
ae61acbedd New translations en.po (German) 2025-10-25 17:09:12 -04:00
hank
af392c8084 New translations en.po (French) 2025-10-25 16:53:16 -04:00
hank
ce8d206004 New translations en.po (Danish) 2025-10-25 06:58:38 -04:00
hank
5a15e7c048 New translations en.po (Danish) 2025-10-25 05:58:43 -04:00
hank
3a360f3ede New translations en.po (Spanish) 2025-10-24 13:47:34 -04:00
hank
7cf2493af7 New translations en.po (Spanish) 2025-10-24 12:17:55 -04:00
hank
d71a0083bb New translations en.po (Chinese Simplified) 2025-10-23 15:40:46 -04:00
hank
6f6aeeb315 New translations en.po (Danish) 2025-10-22 19:14:03 -04:00
hank
7845d25c83 New translations en.po (Danish) 2025-10-22 17:51:32 -04:00
hank
3320707567 New translations en.po (Danish) 2025-10-22 14:17:32 -04:00
hank
e0df2a1e60 New translations en.po (Norwegian) 2025-10-22 06:37:12 -04:00
hank
881c0cd137 New translations en.po (German) 2025-10-22 06:37:11 -04:00
hank
5bde9500b6 New translations en.po (French) 2025-10-21 06:28:47 -04:00
hank
b43541ea60 New translations en.po (German) 2025-10-21 04:08:22 -04:00
hank
339e443bca New translations en.po (Chinese Traditional, Hong Kong) 2025-10-20 17:37:43 -04:00
hank
8643fb2fd5 New translations en.po (Persian) 2025-10-20 17:37:42 -04:00
hank
8dcf03fb15 New translations en.po (Vietnamese) 2025-10-20 17:37:41 -04:00
hank
53c3b0c359 New translations en.po (Chinese Simplified) 2025-10-20 17:37:40 -04:00
hank
1701947b26 New translations en.po (Ukrainian) 2025-10-20 17:37:39 -04:00
hank
c8cb041855 New translations en.po (Turkish) 2025-10-20 17:37:38 -04:00
hank
032d06601e New translations en.po (Swedish) 2025-10-20 17:37:37 -04:00
hank
1507825c16 New translations en.po (Slovenian) 2025-10-20 17:37:36 -04:00
hank
073fc308bb New translations en.po (Russian) 2025-10-20 17:37:35 -04:00
hank
9d5aaaf989 New translations en.po (Polish) 2025-10-20 17:37:34 -04:00
hank
0f6063ebe5 New translations en.po (Korean) 2025-10-20 17:37:33 -04:00
hank
742c217b5f New translations en.po (Japanese) 2025-10-20 17:37:32 -04:00
hank
85589e1e07 New translations en.po (Italian) 2025-10-20 17:37:31 -04:00
hank
6ceb58254b New translations en.po (Greek) 2025-10-20 17:37:30 -04:00
hank
10e21993ce New translations en.po (Danish) 2025-10-20 17:37:29 -04:00
hank
ccff653ef1 New translations en.po (Czech) 2025-10-20 17:37:28 -04:00
hank
323705aced New translations en.po (Bulgarian) 2025-10-20 17:37:27 -04:00
hank
774ddaa726 New translations en.po (Arabic) 2025-10-20 17:37:26 -04:00
hank
e75ada4483 New translations en.po (Spanish) 2025-10-20 17:37:24 -04:00
hank
14e8b28b85 New translations en.po (Romanian) 2025-10-20 17:37:24 -04:00
hank
1f7f764fca New translations en.po (Croatian) 2025-10-20 17:37:23 -04:00
hank
2757e51040 New translations en.po (Chinese Traditional) 2025-10-20 17:37:22 -04:00
hank
1233e6bee6 New translations en.po (Portuguese) 2025-10-20 17:37:21 -04:00
hank
e4619b303e New translations en.po (Norwegian) 2025-10-20 17:37:20 -04:00
hank
3abb7a2a29 New translations en.po (Dutch) 2025-10-20 17:37:19 -04:00
hank
045c3cfdf8 New translations en.po (Hungarian) 2025-10-20 17:37:18 -04:00
hank
4b5e1cc5fa New translations en.po (German) 2025-10-20 17:37:17 -04:00
hank
7600a47d08 New translations en.po (French) 2025-10-20 17:37:16 -04:00
hank
47827c09f6 New translations en.po (French) 2025-10-20 12:38:50 -04:00
hank
c8c84ca0ad New translations en.po (French) 2025-10-20 11:14:32 -04:00
hank
309860f9d0 New translations en.po (Chinese Traditional, Hong Kong) 2025-10-18 19:59:24 -04:00
hank
2a76cf4a1f New translations en.po (Persian) 2025-10-18 19:59:23 -04:00
hank
6d6b6891e1 New translations en.po (Vietnamese) 2025-10-18 19:59:22 -04:00
hank
bdd24b95d2 New translations en.po (Chinese Simplified) 2025-10-18 19:59:21 -04:00
hank
9ae2bee9e3 New translations en.po (Ukrainian) 2025-10-18 19:59:20 -04:00
hank
b2396de0d9 New translations en.po (Turkish) 2025-10-18 19:59:19 -04:00
hank
d85e3bc26f New translations en.po (Swedish) 2025-10-18 19:59:18 -04:00
hank
2a3220be5a New translations en.po (Slovenian) 2025-10-18 19:59:17 -04:00
hank
92910faca0 New translations en.po (Russian) 2025-10-18 19:59:16 -04:00
hank
d596474426 New translations en.po (Polish) 2025-10-18 19:59:15 -04:00
hank
db471ea619 New translations en.po (Korean) 2025-10-18 19:59:13 -04:00
hank
f6e30b1c9f New translations en.po (Japanese) 2025-10-18 19:59:13 -04:00
hank
dcc013330e New translations en.po (Italian) 2025-10-18 19:59:12 -04:00
hank
d6feda8a91 New translations en.po (Greek) 2025-10-18 19:59:11 -04:00
hank
310892d401 New translations en.po (Danish) 2025-10-18 19:59:09 -04:00
hank
302e951bb9 New translations en.po (Czech) 2025-10-18 19:59:08 -04:00
hank
6810270f51 New translations en.po (Bulgarian) 2025-10-18 19:59:07 -04:00
hank
1403f75781 New translations en.po (Arabic) 2025-10-18 19:59:06 -04:00
hank
660a7967f8 New translations en.po (Spanish) 2025-10-18 19:59:05 -04:00
hank
5ad420a6bc New translations en.po (Romanian) 2025-10-18 19:59:04 -04:00
hank
7403f67109 New translations en.po (Croatian) 2025-10-18 19:59:03 -04:00
hank
626b865c3b New translations en.po (Chinese Traditional) 2025-10-18 19:59:02 -04:00
hank
ebbddef0d9 New translations en.po (Portuguese) 2025-10-18 19:59:01 -04:00
hank
a528ddfea3 New translations en.po (Norwegian) 2025-10-18 19:59:00 -04:00
hank
9c6a4873b2 New translations en.po (Dutch) 2025-10-18 19:58:59 -04:00
hank
82e976ff0b New translations en.po (Hungarian) 2025-10-18 19:58:58 -04:00
hank
5342f2cbbc New translations en.po (German) 2025-10-18 19:58:57 -04:00
hank
c7838f744f New translations en.po (French) 2025-10-18 19:58:56 -04:00
hank
74e41851cf New translations en.po (Croatian) 2025-10-18 18:52:43 -04:00
hank
f1342a305c New translations en.po (Hungarian) 2025-10-14 19:40:36 -04:00
hank
7f926c687b New translations en.po (Hungarian) 2025-10-14 16:31:25 -04:00
hank
496cc67390 New translations en.po (Hungarian) 2025-10-14 15:16:43 -04:00
hank
e4b300bc71 New translations en.po (Hungarian) 2025-10-14 14:15:05 -04:00
hank
cee20d701a New translations en.po (Norwegian) 2025-10-13 13:31:12 -04:00
hank
0cd5f3696d New translations en.po (Norwegian) 2025-10-13 11:54:15 -04:00
hank
3686df0f9d New translations en.po (Chinese Traditional) 2025-10-11 23:15:08 -04:00
hank
f58f555367 New translations en.po (French) 2025-10-09 18:25:14 -04:00
hank
adfa14ccbe New translations en.po (Portuguese) 2025-10-09 08:03:07 -04:00
hank
26a147e2e5 New translations en.po (Norwegian) 2025-10-06 03:37:07 -04:00
hank
b9a74e1284 New translations en.po (German) 2025-10-05 12:13:24 -04:00
hank
21d2b3ec7b New translations en.po (Romanian) 2025-10-03 14:42:32 -04:00
hank
69d94b0bf9 New translations en.po (Chinese Traditional, Hong Kong) 2025-10-03 14:42:31 -04:00
hank
5e49fca60e New translations en.po (Croatian) 2025-10-03 14:42:30 -04:00
hank
9babff17d1 New translations en.po (Persian) 2025-10-03 14:42:29 -04:00
hank
be86983f00 New translations en.po (Vietnamese) 2025-10-03 14:42:28 -04:00
hank
907bb4dc52 New translations en.po (Chinese Traditional) 2025-10-03 14:42:27 -04:00
hank
9a34a3700d New translations en.po (Chinese Simplified) 2025-10-03 14:42:26 -04:00
hank
fdb468abf4 New translations en.po (Ukrainian) 2025-10-03 14:42:25 -04:00
hank
0de0326778 New translations en.po (Turkish) 2025-10-03 14:42:24 -04:00
hank
30db58b94f New translations en.po (Swedish) 2025-10-03 14:42:23 -04:00
hank
8e40b1013b New translations en.po (Slovenian) 2025-10-03 14:42:22 -04:00
hank
aa96521696 New translations en.po (Russian) 2025-10-03 14:42:21 -04:00
hank
17f40d58ac New translations en.po (Portuguese) 2025-10-03 14:42:20 -04:00
hank
bdcdda4e9c New translations en.po (Polish) 2025-10-03 14:42:18 -04:00
hank
c36d57f962 New translations en.po (Norwegian) 2025-10-03 14:42:17 -04:00
hank
542ac4bfc0 New translations en.po (Dutch) 2025-10-03 14:42:16 -04:00
hank
68a684f3d6 New translations en.po (Korean) 2025-10-03 14:42:15 -04:00
hank
b9bcb372f7 New translations en.po (Japanese) 2025-10-03 14:42:14 -04:00
hank
3d94451124 New translations en.po (Italian) 2025-10-03 14:42:13 -04:00
hank
0af952d66c New translations en.po (Hungarian) 2025-10-03 14:42:12 -04:00
hank
e46bc1ee36 New translations en.po (Greek) 2025-10-03 14:42:11 -04:00
hank
5d297be871 New translations en.po (German) 2025-10-03 14:42:10 -04:00
hank
6c0bc90f96 New translations en.po (Danish) 2025-10-03 14:42:09 -04:00
hank
db0b6f77e3 New translations en.po (Czech) 2025-10-03 14:42:08 -04:00
hank
7f42ab68d2 New translations en.po (Bulgarian) 2025-10-03 14:42:07 -04:00
hank
c4f6e81c56 New translations en.po (Arabic) 2025-10-03 14:42:06 -04:00
hank
3bf595959b New translations en.po (Spanish) 2025-10-03 14:42:05 -04:00
hank
5af1e058b0 New translations en.po (French) 2025-10-03 14:42:03 -04:00
hank
ec62d1597b New translations en.po (Romanian) 2025-10-03 13:07:14 -04:00
hank
fa06a2935b New translations en.po (Russian) 2025-09-28 03:31:06 -04:00
hank
2cdd521a10 New translations en.po (Portuguese) 2025-09-26 15:48:54 -04:00
hank
90ac853e4f New translations en.po (Slovenian) 2025-09-25 13:11:54 -04:00
hank
007fe0c0af New translations en.po (Croatian) 2025-09-24 18:31:13 -04:00
hank
f3afcd351a New translations en.po (Chinese Traditional) 2025-09-24 18:31:12 -04:00
hank
8eb161171e New translations en.po (Chinese Simplified) 2025-09-24 18:31:11 -04:00
hank
413f829107 New translations en.po (Croatian) 2025-09-23 08:43:25 -04:00
hank
7f09474f33 New translations en.po (Korean) 2025-09-22 22:45:02 -04:00
hank
28386c58db New translations en.po (Chinese Traditional, Hong Kong) 2025-09-22 19:10:36 -04:00
hank
e79aae7925 New translations en.po (Croatian) 2025-09-22 19:10:35 -04:00
hank
fabadf998b New translations en.po (Persian) 2025-09-22 19:10:34 -04:00
hank
2f0d158ed8 New translations en.po (Icelandic) 2025-09-22 19:10:33 -04:00
hank
b5f08d4e4c New translations en.po (Vietnamese) 2025-09-22 19:10:32 -04:00
hank
fce10da7f6 New translations en.po (Chinese Traditional) 2025-09-22 19:10:31 -04:00
hank
d59937dba7 New translations en.po (Chinese Simplified) 2025-09-22 19:10:30 -04:00
hank
0eb2bfab12 New translations en.po (Ukrainian) 2025-09-22 19:10:29 -04:00
hank
bb4111671f New translations en.po (Turkish) 2025-09-22 19:10:28 -04:00
hank
5e50d791fd New translations en.po (Swedish) 2025-09-22 19:10:27 -04:00
hank
cfaf0712d6 New translations en.po (Slovenian) 2025-09-22 19:10:26 -04:00
hank
9cb2d694fe New translations en.po (Russian) 2025-09-22 19:10:25 -04:00
hank
9f4c6b30d8 New translations en.po (Portuguese) 2025-09-22 19:10:24 -04:00
hank
b9055a5d22 New translations en.po (Polish) 2025-09-22 19:10:23 -04:00
hank
aa0d1e7f61 New translations en.po (Norwegian) 2025-09-22 19:10:22 -04:00
hank
35a8cb1d36 New translations en.po (Dutch) 2025-09-22 19:10:21 -04:00
hank
92ba8a0ca3 New translations en.po (Korean) 2025-09-22 19:10:20 -04:00
hank
acbc02162f New translations en.po (Japanese) 2025-09-22 19:10:19 -04:00
hank
8da777b6f4 New translations en.po (Italian) 2025-09-22 19:10:18 -04:00
hank
e62ec1d993 New translations en.po (Hungarian) 2025-09-22 19:10:16 -04:00
hank
4fa11b4c79 New translations en.po (Greek) 2025-09-22 19:10:15 -04:00
hank
c9dba873ee New translations en.po (German) 2025-09-22 19:10:14 -04:00
hank
3e2e897f34 New translations en.po (Danish) 2025-09-22 19:10:13 -04:00
hank
9658fba5aa New translations en.po (Czech) 2025-09-22 19:10:12 -04:00
hank
1c00c39eac New translations en.po (Bulgarian) 2025-09-22 19:10:11 -04:00
hank
2d3f186c18 New translations en.po (Arabic) 2025-09-22 19:10:10 -04:00
hank
983a471e6f New translations en.po (Spanish) 2025-09-22 19:10:08 -04:00
hank
7a228f553c New translations en.po (French) 2025-09-22 19:10:08 -04:00
hank
007dfa9519 New translations en.po (Dutch) 2025-09-21 14:30:42 -04:00
hank
f3a74b1f46 New translations en.po (Polish) 2025-09-18 11:36:03 -04:00
hank
4a76b620e7 New translations en.po (Polish) 2025-09-18 08:40:40 -04:00
hank
25210d031d New translations en.po (Chinese Traditional, Hong Kong) 2025-09-17 14:45:45 -04:00
hank
87b55fd4cf New translations en.po (Croatian) 2025-09-17 14:45:44 -04:00
hank
b4430ac76f New translations en.po (Persian) 2025-09-17 14:45:43 -04:00
hank
5f9aa78b72 New translations en.po (Icelandic) 2025-09-17 14:45:42 -04:00
hank
7537f6bd5c New translations en.po (Vietnamese) 2025-09-17 14:45:41 -04:00
hank
83dee5e554 New translations en.po (Chinese Traditional) 2025-09-17 14:45:40 -04:00
hank
036b4495e6 New translations en.po (Chinese Simplified) 2025-09-17 14:45:39 -04:00
hank
31cd36fcc1 New translations en.po (Ukrainian) 2025-09-17 14:45:38 -04:00
hank
0a2aaf3260 New translations en.po (Turkish) 2025-09-17 14:45:36 -04:00
hank
6e2e90120a New translations en.po (Swedish) 2025-09-17 14:45:35 -04:00
hank
23f95d6ebd New translations en.po (Slovenian) 2025-09-17 14:45:35 -04:00
hank
56dc1096d9 New translations en.po (Russian) 2025-09-17 14:45:34 -04:00
hank
9dd203f85a New translations en.po (Portuguese) 2025-09-17 14:45:32 -04:00
hank
adac9cf79d New translations en.po (Polish) 2025-09-17 14:45:31 -04:00
hank
efd2ba04c5 New translations en.po (Norwegian) 2025-09-17 14:45:30 -04:00
hank
a9055e216d New translations en.po (Dutch) 2025-09-17 14:45:29 -04:00
hank
f64130029b New translations en.po (Korean) 2025-09-17 14:45:28 -04:00
hank
6d172bac82 New translations en.po (Japanese) 2025-09-17 14:45:27 -04:00
hank
354cba5690 New translations en.po (Italian) 2025-09-17 14:45:26 -04:00
hank
e65cde9675 New translations en.po (Hungarian) 2025-09-17 14:45:25 -04:00
hank
fbd2fbb6a6 New translations en.po (Greek) 2025-09-17 14:45:24 -04:00
hank
01bf64083a New translations en.po (German) 2025-09-17 14:45:23 -04:00
hank
103856121d New translations en.po (Danish) 2025-09-17 14:45:21 -04:00
hank
8a798c7e3f New translations en.po (Czech) 2025-09-17 14:45:20 -04:00
hank
beeec5c39e New translations en.po (Bulgarian) 2025-09-17 14:45:19 -04:00
hank
6d43045d79 New translations en.po (Arabic) 2025-09-17 14:45:18 -04:00
hank
88ea94f5b0 New translations en.po (Spanish) 2025-09-17 14:45:17 -04:00
hank
3b8d333f8e New translations en.po (French) 2025-09-17 14:45:16 -04:00
hank
3a06982502 New translations en.po (Chinese Simplified) 2025-09-17 01:21:38 -04:00
hank
b820b46042 New translations en.po (Polish) 2025-09-14 04:46:36 -04:00
hank
fab799f177 New translations en.po (Chinese Traditional, Hong Kong) 2025-09-11 12:53:01 -04:00
hank
5eaf9b9157 New translations en.po (Croatian) 2025-09-11 12:53:00 -04:00
hank
1eed3c53c8 New translations en.po (Persian) 2025-09-11 12:52:59 -04:00
hank
90729a7a95 New translations en.po (Icelandic) 2025-09-11 12:52:58 -04:00
hank
d450f6df10 New translations en.po (Vietnamese) 2025-09-11 12:52:57 -04:00
hank
7aa2bcf761 New translations en.po (Chinese Traditional) 2025-09-11 12:52:55 -04:00
hank
a7a86f46c3 New translations en.po (Chinese Simplified) 2025-09-11 12:52:54 -04:00
hank
17e30aff60 New translations en.po (Ukrainian) 2025-09-11 12:52:53 -04:00
hank
66008e47f3 New translations en.po (Turkish) 2025-09-11 12:52:52 -04:00
hank
56788b1e5b New translations en.po (Swedish) 2025-09-11 12:52:51 -04:00
hank
b72371487a New translations en.po (Slovenian) 2025-09-11 12:52:50 -04:00
hank
7656b4189e New translations en.po (Russian) 2025-09-11 12:52:49 -04:00
hank
8e6731c102 New translations en.po (Portuguese) 2025-09-11 12:52:48 -04:00
hank
e86fa40fe4 New translations en.po (Polish) 2025-09-11 12:52:46 -04:00
hank
f0e728a1ed New translations en.po (Norwegian) 2025-09-11 12:52:45 -04:00
hank
bb076eb439 New translations en.po (Dutch) 2025-09-11 12:52:44 -04:00
hank
2f0b16367a New translations en.po (Korean) 2025-09-11 12:52:43 -04:00
hank
aa33124e18 New translations en.po (Japanese) 2025-09-11 12:52:42 -04:00
hank
42f404c80a New translations en.po (Italian) 2025-09-11 12:52:41 -04:00
hank
5056fddd40 New translations en.po (Hungarian) 2025-09-11 12:52:40 -04:00
hank
2296202ea1 New translations en.po (Greek) 2025-09-11 12:52:39 -04:00
hank
c034e9b0fa New translations en.po (German) 2025-09-11 12:52:38 -04:00
hank
2d45119a98 New translations en.po (Danish) 2025-09-11 12:52:36 -04:00
hank
63be4f1ab5 New translations en.po (Czech) 2025-09-11 12:52:35 -04:00
hank
6d2259100e New translations en.po (Bulgarian) 2025-09-11 12:52:34 -04:00
hank
142af6e7b6 New translations en.po (Arabic) 2025-09-11 12:52:33 -04:00
hank
5c1e009188 New translations en.po (Spanish) 2025-09-11 12:52:32 -04:00
hank
27b2cb84d6 New translations en.po (French) 2025-09-11 12:52:31 -04:00
hank
8db87e5497 Update Crowdin configuration file 2025-09-11 12:45:43 -04:00
henrygd
e601a0d564 add TRUSTED_AUTH_HEADER for auth forwarding (#399) 2025-09-10 21:26:59 -04:00
Fankesyooni
07491108cd new zh-CN translations (#1160) 2025-09-10 13:34:17 -04:00
henrygd
42ab17de1f move i18n.yml to project root 2025-09-10 13:30:42 -04:00
henrygd
2d14174f61 update i18n.yml 2025-09-10 13:28:06 -04:00
henrygd
a19ccc9263 comments 2025-09-09 17:13:15 -04:00
henrygd
956880aa59 improve container filtering performance
- also a few instances of autoformat
2025-09-09 16:59:16 -04:00
henrygd
b2b54db409 update makefile with proper site src path 2025-09-09 15:41:15 -04:00
henrygd
32d5188eef path updates after reorganization 2025-09-09 13:53:36 -04:00
henrygd
46dab7f531 fix gitignore after reorganization 2025-09-09 13:51:46 -04:00
henrygd
c898a9ebbc update /src to /internal 2025-09-09 13:35:34 -04:00
henrygd
8a13b05c20 rename /src to /internal (sorry i'll fix the prs) 2025-09-09 13:29:07 -04:00
henrygd
86ea23fe39 refactor install-agent.sh for freebsd
- Updated paths for FreeBSD installation, changing default directories from /opt/beszel-agent to /usr/local/etc/beszel-agent and /usr/local/sbin.
- Introduced conditional path setting based on the operating system.
- Updated cron job creation to use /etc/cron.d
2025-09-08 19:46:39 -04:00
henrygd
a284dd74dd add time format (12h, 24h) settings (#424)
- Some biome formatting :/
- Changed chartTime update to use listenkeys
2025-09-08 17:12:43 -04:00
Alexander Mnich
6a0075291c [FIX] OpenWRT auto update (#1155)
* switch to root crontab for OpenWRT auto update

* fix: OpenWRT auto update restart condition
2025-09-08 16:50:39 -04:00
henrygd
f542bc70a1 add biome and apply selected formatting / fixes 2025-09-08 15:22:04 -04:00
henrygd
270e59d9ea add support for otp and mfa 2025-09-07 20:46:34 -04:00
henrygd
0d97a604f8 rename main.go to beszel.go 2025-09-07 17:34:56 -04:00
henrygd
f6078fc232 fix govulncheck workflow 2025-09-07 16:52:46 -04:00
henrygd
6f5d95031c update project structure
- move agent to /agent
- change /beszel to /src
- update workflows and docker builds
2025-09-07 16:42:15 -04:00
henrygd
4e26defdca update imports for gh package name 2025-09-07 14:48:39 -04:00
Ayman Nedjmeddine
cda8fa7efd Rename Go module to github.com/henrygd/beszel 2025-09-07 14:29:56 -04:00
henrygd
fd050f2a8f revert to previous version behavior of setting hub.appURL (#1148)
- remove fallback that sets appUrl to settings.Meta.AppURL
- prefer using current browser URL in generated config if APP_URL not set
2025-09-07 14:10:47 -04:00
henrygd
e53d41dcec refactor: launch web url for dev server + css update 2025-09-07 14:10:34 -04:00
Sasha Blue
a1eb15dabb Adding openwrt restart procedure after updating the agent automatically (#1151) 2025-09-07 13:25:23 -04:00
henrygd
eb4bdafbea add freebsd support to agent install script / update command (#39) 2025-09-05 19:15:21 -04:00
Alexander Mnich
fea2330534 fix: avoid goreleaser truthy evaluation (#1146)
Goreleaser performs truthy evaluation on templates. As .Env.IS_FORK is a string the env variable containing a non empty-string already evaluated to true.
2025-09-05 17:25:57 -04:00
henrygd
5e37469ea9 0.12.7 release :) 2025-09-05 14:00:24 -04:00
henrygd
e027479bb1 make sure initial user is verfied when supplying user/pass 2025-09-05 14:00:21 -04:00
henrygd
1597e869c1 update translations 2025-09-05 13:53:17 -04:00
henrygd
862399d8ec update language files 2025-09-05 12:36:45 -04:00
henrygd
f6f85f8f9d Add USER_EMAIL and USER_PASSWORD env vars to set the email / pass of initial user (#1137) 2025-09-05 11:42:43 -04:00
Riedel, Max
e22d7ca801 fix: add nextSystemToken to deps of useEffect to generate a new token after system creation (#1142) 2025-09-05 11:00:32 -04:00
henrygd
c382c1d5f6 windows: make LHM opt-in with LHM=true (#1130) 2025-09-05 10:39:18 -04:00
henrygd
f7618ed6b0 update go version for vulcheck action 2025-09-04 19:17:44 -04:00
henrygd
d1295b7c50 alerts tests and small refactoring 2025-09-04 19:13:10 -04:00
henrygd
a162a54a58 bump go version and add keyword 2025-09-04 19:13:10 -04:00
henrygd
794db0ac6a make sure old names are removed in systemsbyname store 2025-09-04 19:13:10 -04:00
henrygd
e9fb9b856f install script: remove newlines from KEY (#1139) 2025-09-04 11:26:53 -04:00
Sven van Ginkel
66bca11d36 [Bug] Update install script to use crontab on Alpine (#1136)
* add cron

* update the install script
2025-09-03 23:10:38 -04:00
henrygd
86e87f0d47 refactor hub dev server
- moved html replacement functionality from vite to go
2025-09-01 22:16:57 -04:00
henrygd
fadfc5d81d refactor(hub): separate development and production server logic 2025-09-01 19:27:11 -04:00
henrygd
fc39ff1e4d add pflag to go deps 2025-09-01 19:24:26 -04:00
henrygd
82ccfc66e0 refactor: shared container charts config hook 2025-09-01 18:41:30 -04:00
henrygd
890bad1c39 refactor: improve runOnce with weakmap cache 2025-09-01 18:34:29 -04:00
henrygd
9c458885f1 refactor (hub): add systemsManager module
- Removed the `updateSystemList` function and replaced it with a more efficient system management approach using `systemsManager`.
- Updated the `App` component to initialize and subscribe to system updates through the new `systemsManager`.
- Refactored the `SystemsTable` and `SystemDetail` components to utilize the new state management for systems, improving performance and maintainability.
- Enhanced the `ActiveAlerts` component to fetch system names directly from the new state structure.
2025-09-01 17:29:33 -04:00
henrygd
d2aed0dc72 refactor: replace useLocalStorage with useBrowserStorage 2025-09-01 17:28:13 -04:00
232 changed files with 27811 additions and 2805 deletions

48
.dockerignore Normal file
View File

@@ -0,0 +1,48 @@
# Node.js dependencies
node_modules
internalsite/node_modules
# Go build artifacts and binaries
build
dist
*.exe
beszel-agent
beszel_data*
pb_data
data
temp
# Development and IDE files
.vscode
.idea*
*.swc
__debug_*
# Git and version control
.git
.gitignore
# Documentation and supplemental files
*.md
supplemental
freebsd-port
# Test files (exclude from production builds)
*_test.go
coverage
# Docker files
dockerfile_*
# Temporary files
*.tmp
*.bak
*.log
# OS specific files
.DS_Store
Thumbs.db
# .NET build artifacts
agent/lhm/obj
agent/lhm/bin

View File

@@ -13,44 +13,44 @@ jobs:
matrix:
include:
- image: henrygd/beszel
context: ./beszel
dockerfile: ./beszel/dockerfile_hub
context: ./
dockerfile: ./internal/dockerfile_hub
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: henrygd/beszel-agent
context: ./beszel
dockerfile: ./beszel/dockerfile_agent
context: ./
dockerfile: ./internal/dockerfile_agent
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: henrygd/beszel-agent-nvidia
context: ./beszel
dockerfile: ./beszel/dockerfile_agent_nvidia
context: ./
dockerfile: ./internal/dockerfile_agent_nvidia
platforms: linux/amd64
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: ghcr.io/${{ github.repository }}/beszel
context: ./beszel
dockerfile: ./beszel/dockerfile_hub
context: ./
dockerfile: ./internal/dockerfile_hub
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
- image: ghcr.io/${{ github.repository }}/beszel-agent
context: ./beszel
dockerfile: ./beszel/dockerfile_agent
context: ./
dockerfile: ./internal/dockerfile_agent
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
context: ./beszel
dockerfile: ./beszel/dockerfile_agent_nvidia
context: ./
dockerfile: ./internal/dockerfile_agent_nvidia
platforms: linux/amd64
registry: ghcr.io
username: ${{ github.actor }}
@@ -68,10 +68,10 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./beszel/site
run: bun install --no-save --cwd ./internal/site
- name: Build site
run: bun run --cwd ./beszel/site build
run: bun run --cwd ./internal/site build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -93,8 +93,8 @@ jobs:
# https://github.com/docker/login-action
- name: Login to Docker Hub
env:
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
env:
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
uses: docker/login-action@v3
with:

View File

@@ -21,10 +21,10 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./beszel/site
run: bun install --no-save --cwd ./internal/site
- name: Build site
run: bun run --cwd ./beszel/site build
run: bun run --cwd ./internal/site build
- name: Set up Go
uses: actions/setup-go@v5
@@ -38,13 +38,13 @@ jobs:
- name: Build .NET LHM executable for Windows sensors
run: |
dotnet build -c Release ./beszel/internal/agent/lhm/beszel_lhm.csproj
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj
shell: bash
- name: GoReleaser beszel
uses: goreleaser/goreleaser-action@v6
with:
workdir: ./beszel
workdir: ./
distribution: goreleaser
version: latest
args: release --clean

View File

@@ -15,7 +15,7 @@ permissions:
jobs:
vulncheck:
name: Analysis
name: VulnCheck
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
@@ -23,11 +23,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
cached: false
go-version: 1.25.x
# cached: false
- name: Get official govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
shell: bash
- name: Run govulncheck
run: govulncheck -C ./beszel -show verbose ./...
run: govulncheck -show verbose ./...
shell: bash

12
.gitignore vendored
View File

@@ -8,15 +8,15 @@ beszel_data
beszel_data*
dist
*.exe
beszel/cmd/hub/hub
beszel/cmd/agent/agent
internal/cmd/hub/hub
internal/cmd/agent/agent
node_modules
beszel/build
build
*timestamp*
.swc
beszel/site/src/locales/**/*.ts
internal/site/src/locales/**/*.ts
*.bak
__debug_*
beszel/internal/agent/lhm/obj
beszel/internal/agent/lhm/bin
agent/lhm/obj
agent/lhm/bin
dockerfile_agent_dev

View File

@@ -9,7 +9,7 @@ before:
builds:
- id: beszel
binary: beszel
main: cmd/hub/hub.go
main: internal/cmd/hub/hub.go
env:
- CGO_ENABLED=0
goos:
@@ -22,7 +22,7 @@ builds:
- id: beszel-agent
binary: beszel-agent
main: cmd/agent/agent.go
main: internal/cmd/agent/agent.go
env:
- CGO_ENABLED=0
goos:
@@ -103,28 +103,28 @@ nfpms:
formats:
- deb
contents:
- src: ../supplemental/debian/beszel-agent.service
- src: ./supplemental/debian/beszel-agent.service
dst: lib/systemd/system/beszel-agent.service
packager: deb
- src: ../supplemental/debian/copyright
- src: ./supplemental/debian/copyright
dst: usr/share/doc/beszel-agent/copyright
packager: deb
- src: ../supplemental/debian/lintian-overrides
- src: ./supplemental/debian/lintian-overrides
dst: usr/share/lintian/overrides/beszel-agent
packager: deb
scripts:
postinstall: ../supplemental/debian/postinstall.sh
preremove: ../supplemental/debian/prerm.sh
postremove: ../supplemental/debian/postrm.sh
postinstall: ./supplemental/debian/postinstall.sh
preremove: ./supplemental/debian/prerm.sh
postremove: ./supplemental/debian/postrm.sh
deb:
predepends:
- adduser
- debconf
scripts:
templates: ../supplemental/debian/templates
templates: ./supplemental/debian/templates
# Currently broken due to a bug in goreleaser
# https://github.com/goreleaser/goreleaser/issues/5487
#config: ../supplemental/debian/config.sh
#config: ./supplemental/debian/config.sh
scoops:
- ids: [beszel-agent]
@@ -135,7 +135,7 @@ scoops:
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
# chocolateys:
@@ -169,7 +169,7 @@ brews:
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
extra_install: |
(bin/"beszel-agent-launcher").write <<~EOS
#!/bin/bash
@@ -201,7 +201,7 @@ winget:
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
publisher_support_url: "https://github.com/henrygd/beszel/issues"
short_description: "Agent for Beszel, a lightweight server monitoring platform."
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
description: |
Beszel is a lightweight server monitoring platform that includes Docker
statistics, historical data, and alert functions. It has a friendly web

102
Makefile Normal file
View File

@@ -0,0 +1,102 @@
# Default OS/ARCH values
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
# Skip building the web UI if true
SKIP_WEB ?= false
# Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales
.DEFAULT_GOAL := build
clean:
go clean
rm -rf ./build
lint:
golangci-lint run
test: export GOEXPERIMENT=synctest
test:
go test -tags=testing ./...
tidy:
go mod tidy
build-web-ui:
@if command -v bun >/dev/null 2>&1; then \
bun install --cwd ./internal/site && \
bun run --cwd ./internal/site build; \
else \
npm install --prefix ./internal/site && \
npm run --prefix ./internal/site build; \
fi
# Conditional .NET build - only for Windows
build-dotnet-conditional:
@if [ "$(OS)" = "windows" ]; then \
echo "Building .NET executable for Windows..."; \
if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./agent/lhm/bin; \
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
else \
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
exit 1; \
fi; \
fi
# Update build-agent to include conditional .NET build
build-agent: tidy build-dotnet-conditional
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/agent
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
build-hub-dev: tidy
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
build: build-agent build-hub
generate-locales:
@if [ ! -f ./internal/site/src/locales/en/en.ts ]; then \
echo "Generating locales..."; \
command -v bun >/dev/null 2>&1 && cd ./internal/site && bun install && bun run sync || cd ./internal/site && npm install && npm run sync; \
fi
dev-server: generate-locales
cd ./internal/site
@if command -v bun >/dev/null 2>&1; then \
cd ./internal/site && bun run dev --host 0.0.0.0; \
else \
cd ./internal/site && npm run dev --host 0.0.0.0; \
fi
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/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
dev-agent:
@if command -v entr >/dev/null 2>&1; then \
find ./internal/cmd/agent/*.go ./agent/*.go | entr -r go run github.com/henrygd/beszel/internal/cmd/agent; \
else \
go run github.com/henrygd/beszel/internal/cmd/agent; \
fi
build-dotnet:
@if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./agent/lhm/bin; \
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
else \
echo "dotnet not found"; \
fi
# KEY="..." make -j dev
dev: dev-server dev-hub dev-agent

View File

@@ -1,9 +1,10 @@
// Package agent handles the agent's SSH server and system stats collection.
// Package agent implements the Beszel monitoring agent that collects and serves system metrics.
//
// The agent runs on monitored systems and communicates collected data
// to the Beszel hub for centralized monitoring and alerting.
package agent
import (
"beszel"
"beszel/internal/entities/system"
"crypto/sha256"
"encoding/hex"
"log/slog"
@@ -14,6 +15,8 @@ import (
"time"
"github.com/gliderlabs/ssh"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/host"
gossh "golang.org/x/crypto/ssh"
)

View File

@@ -1,8 +1,9 @@
package agent
import (
"beszel/internal/entities/system"
"time"
"github.com/henrygd/beszel/internal/entities/system"
)
// Not thread safe since we only access from gatherStats which is already locked

View File

@@ -4,11 +4,12 @@
package agent
import (
"beszel/internal/entities/system"
"testing"
"testing/synctest"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -1,8 +1,6 @@
package agent
import (
"beszel"
"beszel/internal/common"
"crypto/tls"
"errors"
"fmt"
@@ -15,6 +13,9 @@ import (
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/lxzan/gws"
"golang.org/x/crypto/ssh"

View File

@@ -4,8 +4,6 @@
package agent
import (
"beszel"
"beszel/internal/common"
"crypto/ed25519"
"net/url"
"os"
@@ -13,6 +11,10 @@ import (
"testing"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@@ -1,13 +1,14 @@
package agent
import (
"beszel/internal/agent/health"
"errors"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/henrygd/beszel/agent/health"
)
// ConnectionManager manages the connection state and events for the agent.

View File

@@ -1,7 +1,6 @@
package agent
import (
"beszel/internal/entities/system"
"log/slog"
"os"
"path/filepath"
@@ -9,6 +8,8 @@ import (
"strings"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/disk"
)

View File

@@ -1,7 +1,6 @@
package agent
import (
"beszel/internal/entities/container"
"bytes"
"context"
"encoding/json"
@@ -15,6 +14,8 @@ import (
"sync"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/blang/semver"
)

View File

@@ -1,7 +1,6 @@
package agent
import (
"beszel/internal/entities/system"
"bufio"
"bytes"
"encoding/json"
@@ -13,6 +12,8 @@ import (
"sync"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"golang.org/x/exp/slog"
)

View File

@@ -4,12 +4,13 @@
package agent
import (
"beszel/internal/entities/system"
"os"
"path/filepath"
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -1,7 +1,6 @@
package agent
import (
"beszel/internal/entities/system"
"context"
"fmt"
"log/slog"
@@ -11,6 +10,8 @@ import (
"strings"
"unicode/utf8"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/common"
"github.com/shirou/gopsutil/v4/sensors"
)

View File

@@ -4,12 +4,13 @@
package agent
import (
"beszel/internal/entities/system"
"context"
"fmt"
"os"
"testing"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/common"
"github.com/shirou/gopsutil/v4/sensors"
"github.com/stretchr/testify/assert"

View File

@@ -46,9 +46,10 @@ var lhmFs embed.FS
var (
beszelLhm *lhmProcess
beszelLhmOnce sync.Once
useLHM = os.Getenv("LHM") == "true"
)
var errNoSensors = errors.New("no sensors found (try running as admin)")
var errNoSensors = errors.New("no sensors found (try running as admin with LHM=true)")
// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.
func newlhmProcess() (*lhmProcess, error) {
@@ -139,7 +140,7 @@ func (lhm *lhmProcess) cleanupProcess() {
}
func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
if lhm.stoppedNoSensors {
if !useLHM || lhm.stoppedNoSensors {
// Fall back to gopsutil if we can't get sensors from LHM
return sensors.TemperaturesWithContext(ctx)
}
@@ -222,6 +223,10 @@ func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err e
}
}()
if !useLHM {
return sensors.TemperaturesWithContext(ctx)
}
// Initialize process once
beszelLhmOnce.Do(func() {
beszelLhm, err = newlhmProcess()

View File

@@ -1,9 +1,6 @@
package agent
import (
"beszel"
"beszel/internal/common"
"beszel/internal/entities/system"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +11,10 @@ import (
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/blang/semver"
"github.com/fxamacker/cbor/v2"
"github.com/gliderlabs/ssh"

View File

@@ -1,8 +1,6 @@
package agent
import (
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"context"
"crypto/ed25519"
"encoding/json"
@@ -15,6 +13,9 @@ import (
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/blang/semver"
"github.com/fxamacker/cbor/v2"
"github.com/gliderlabs/ssh"

View File

@@ -1,9 +1,6 @@
package agent
import (
"beszel"
"beszel/internal/agent/battery"
"beszel/internal/entities/system"
"bufio"
"fmt"
"log/slog"
@@ -12,6 +9,10 @@ import (
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"

View File

@@ -1,12 +1,14 @@
package agent
import (
"beszel/internal/ghupdate"
"fmt"
"log"
"os"
"os/exec"
"runtime"
"strings"
"github.com/henrygd/beszel/internal/ghupdate"
)
// restarter knows how to restart the beszel-agent service.
@@ -45,6 +47,16 @@ func (w *openWRTRestarter) Restart() error {
return exec.Command(w.cmd, "restart", "beszel-agent").Run()
}
type freeBSDRestarter struct{ cmd string }
func (f *freeBSDRestarter) Restart() error {
if err := exec.Command(f.cmd, "beszel-agent", "status").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via FreeBSD rc…")
return exec.Command(f.cmd, "beszel-agent", "restart").Run()
}
func detectRestarter() restarter {
if path, err := exec.LookPath("systemctl"); err == nil {
return &systemdRestarter{cmd: path}
@@ -53,6 +65,9 @@ func detectRestarter() restarter {
return &openRCRestarter{cmd: path}
}
if path, err := exec.LookPath("service"); err == nil {
if runtime.GOOS == "freebsd" {
return &freeBSDRestarter{cmd: path}
}
return &openWRTRestarter{cmd: path}
}
return nil

15
beszel.go Normal file
View File

@@ -0,0 +1,15 @@
// Package beszel provides core application constants and version information
// which are used throughout the application.
package beszel
import "github.com/blang/semver"
const (
// Version is the current version of the application.
Version = "0.12.7"
// AppName is the name of the application.
AppName = "beszel"
)
// MinVersionCbor is the minimum supported version for CBOR compatibility.
var MinVersionCbor = semver.MustParse("0.12.0")

View File

@@ -1,98 +0,0 @@
# Default OS/ARCH values
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
# Skip building the web UI if true
SKIP_WEB ?= false
# Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
.DEFAULT_GOAL := build
clean:
go clean
rm -rf ./build
lint:
golangci-lint run
test: export GOEXPERIMENT=synctest
test:
go test -tags=testing ./...
tidy:
go mod tidy
build-web-ui:
@if command -v bun >/dev/null 2>&1; then \
bun install --cwd ./site && \
bun run --cwd ./site build; \
else \
npm install --prefix ./site && \
npm run --prefix ./site build; \
fi
# Conditional .NET build - only for Windows
build-dotnet-conditional:
@if [ "$(OS)" = "windows" ]; then \
echo "Building .NET executable for Windows..."; \
if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./internal/agent/lhm/bin; \
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
else \
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
exit 1; \
fi; \
fi
# Update build-agent to include conditional .NET build
build-agent: tidy build-dotnet-conditional
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/agent
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
build: build-agent build-hub
generate-locales:
@if [ ! -f ./site/src/locales/en/en.ts ]; then \
echo "Generating locales..."; \
command -v bun >/dev/null 2>&1 && cd ./site && bun install && bun run sync || cd ./site && npm install && npm run sync; \
fi
dev-server: generate-locales
cd ./site
@if command -v bun >/dev/null 2>&1; then \
cd ./site && bun run dev --host 0.0.0.0; \
else \
cd ./site && npm run dev --host 0.0.0.0; \
fi
dev-hub: export ENV=dev
dev-hub:
mkdir -p ./site/dist && touch ./site/dist/index.html
@if command -v entr >/dev/null 2>&1; then \
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
else \
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
fi
dev-agent:
@if command -v entr >/dev/null 2>&1; then \
find ./cmd/agent/*.go ./internal/agent/*.go | entr -r go run beszel/cmd/agent; \
else \
go run beszel/cmd/agent; \
fi
build-dotnet:
@if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./internal/agent/lhm/bin; \
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
else \
echo "dotnet not found"; \
fi
# KEY="..." make -j dev
dev: dev-server dev-hub dev-agent

View File

@@ -1,29 +0,0 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
const (
TempAdminEmail = "_@b.b"
)
func init() {
m.Register(func(app core.App) error {
// initial settings
settings := app.Settings()
settings.Meta.AppName = "Beszel"
settings.Meta.HideControls = true
settings.Logs.MinLevel = 4
if err := app.Save(settings); err != nil {
return err
}
// create superuser
collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
user := core.NewRecord(collection)
user.SetEmail(TempAdminEmail)
user.SetRandomPassword()
return app.Save(user)
}, nil)
}

Binary file not shown.

View File

@@ -1,24 +0,0 @@
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@@ -1,136 +0,0 @@
import { ChartTimes, SystemRecord, UserSettings } from "@/types"
import { $alerts, $longestSystemNameLen, $systems, $userSettings } from "./stores"
import { toast } from "@/components/ui/use-toast"
import { t } from "@lingui/core/macro"
import { chartTimeData } from "./utils"
import { WritableAtom } from "nanostores"
import { RecordModel, RecordSubscription } from "pocketbase"
import PocketBase from "pocketbase"
import { basePath } from "@/components/router"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
export const isAdmin = () => pb.authStore.record?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$systems.set([])
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
/** Update systems / alerts list when records change */
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
const curRecords = $store.get()
const newRecords = []
if (e.action === "delete") {
for (const server of curRecords) {
if (server.id !== e.record.id) {
newRecords.push(server)
}
}
} else {
let found = 0
for (const server of curRecords) {
if (server.id === e.record.id) {
found = newRecords.push(e.record)
} else {
newRecords.push(server)
}
}
if (!found) {
newRecords.push(e.record)
}
}
$store.set(newRecords)
}
/** Fetches updated system list from database */
export const updateSystemList = (() => {
let isFetchingSystems = false
return async () => {
if (isFetchingSystems) {
return
}
isFetchingSystems = true
try {
let records = await pb
.collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) {
// records = [
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ]
// we need to loop once to get the longest name
let longestName = $longestSystemNameLen.get()
for (const { name } of records) {
const nameLen = Math.min(20, name.length)
if (nameLen > longestName) {
$longestSystemNameLen.set(nameLen)
longestName = nameLen
}
}
$systems.set(records)
} else {
verifyAuth()
}
} finally {
isFetchingSystems = false
}
}
})()
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

View File

@@ -1,10 +0,0 @@
package beszel
import "github.com/blang/semver"
const (
Version = "0.12.6"
AppName = "beszel"
)
var MinVersionCbor = semver.MustParse("0.12.0")

View File

@@ -1,6 +1,6 @@
module beszel
module github.com/henrygd/beszel
go 1.24.4
go 1.25.1
// lock shoutrrr to specific version to allow review before updating
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
@@ -18,6 +18,7 @@ require (
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.0
golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
@@ -49,7 +50,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect

View File

@@ -1,3 +1,3 @@
files:
- source: /beszel/site/src/locales/en/en.po
translation: /beszel/site/src/locales/%two_letters_code%/%two_letters_code%.po
- source: /internal/site/src/locales/en/
translation: /internal/site/src/locales/%two_letters_code%/%two_letters_code%.po

View File

@@ -87,7 +87,7 @@ var supportsTitle = map[string]struct{}{
func NewAlertManager(app hubLike) *AlertManager {
am := &AlertManager{
hub: app,
alertQueue: make(chan alertTask),
alertQueue: make(chan alertTask, 5),
stopChan: make(chan struct{}),
}
am.bindEvents()

View File

@@ -42,21 +42,10 @@ func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
// resolveAlertHistoryRecord sets the resolved field to the current time
func resolveAlertHistoryRecord(app core.App, alertRecordID string) error {
alertHistoryRecords, err := app.FindRecordsByFilter(
"alerts_history",
"alert_id={:alert_id} && resolved=null",
"-created",
1,
0,
dbx.Params{"alert_id": alertRecordID},
)
if err != nil {
alertHistoryRecord, err := app.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id} && resolved=null", dbx.Params{"alert_id": alertRecordID})
if err != nil || alertHistoryRecord == nil {
return err
}
if len(alertHistoryRecords) == 0 {
return nil
}
alertHistoryRecord := alertHistoryRecords[0] // there should be only one record
alertHistoryRecord.Set("resolved", time.Now().UTC())
err = app.Save(alertHistoryRecord)
if err != nil {

View File

@@ -1,12 +1,13 @@
package alerts
import (
"beszel/internal/entities/system"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"

View File

@@ -10,8 +10,10 @@ import (
"net/http"
"strings"
"testing"
"testing/synctest"
"time"
beszelTests "beszel/internal/tests"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
@@ -63,14 +65,14 @@ func TestUserAlertsApi(t *testing.T) {
}
scenarios := []beszelTests.ApiScenario{
{
Name: "GET not implemented - returns index",
Method: http.MethodGet,
URL: "/api/beszel/user-alerts",
ExpectedStatus: 200,
ExpectedContent: []string{"<html ", "globalThis.BESZEL"},
TestAppFactory: testAppFactory,
},
// {
// Name: "GET not implemented - returns index",
// Method: http.MethodGet,
// URL: "/api/beszel/user-alerts",
// ExpectedStatus: 200,
// ExpectedContent: []string{"<html ", "globalThis.BESZEL"},
// TestAppFactory: testAppFactory,
// },
{
Name: "POST no auth",
Method: http.MethodPost,
@@ -366,3 +368,237 @@ func TestUserAlertsApi(t *testing.T) {
scenario.Test(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 := getHubWithUser(t)
defer hub.Cleanup()
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
assert.NoError(t, err)
var alerts []*core.Record
for i, system := range systems {
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": i + 1,
})
assert.NoError(t, err)
alerts = append(alerts, alert)
}
time.Sleep(10 * time.Millisecond)
for _, alert := range alerts {
assert.False(t, alert.GetBool("triggered"), "Alert should not be triggered immediately")
}
if hub.TestMailer.TotalSend() != 0 {
assert.Zero(t, hub.TestMailer.TotalSend(), "Expected 0 messages, got %d", hub.TestMailer.TotalSend())
}
for _, system := range systems {
assert.EqualValues(t, "paused", system.GetString("status"), "System should be paused")
}
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
for _, system := range systems {
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
// after 30 seconds, should have 4 alerts in the pendingAlerts map, no triggered alerts
time.Sleep(time.Second * 30)
assert.EqualValues(t, 4, hub.GetPendingAlertsCount(), "should have 4 alerts in the pendingAlerts map")
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 0, triggeredCount, "should have 0 alert triggered")
assert.EqualValues(t, 0, hub.TestMailer.TotalSend(), "should have 0 messages sent")
// after 1:30 seconds, should have 1 triggered alert and 3 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 3, hub.GetPendingAlertsCount(), "should have 3 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "should have 1 alert triggered")
assert.EqualValues(t, 1, hub.TestMailer.TotalSend(), "should have 1 messages sent")
// after 2:30 seconds, should have 2 triggered alerts and 2 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 2, hub.GetPendingAlertsCount(), "should have 2 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 2, triggeredCount, "should have 2 alert triggered")
assert.EqualValues(t, 2, hub.TestMailer.TotalSend(), "should have 2 messages sent")
// now we will bring the remaning systems back up
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
// should have 0 alerts in the pendingAlerts map and 0 alerts triggered
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "should have 0 alert triggered")
// 4 messages sent, 2 down alerts and 2 up alerts for first 2 systems
assert.EqualValues(t, 4, hub.TestMailer.TotalSend(), "should have 4 messages sent")
})
}
func TestAlertsHistory(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
defer hub.Cleanup()
// Create systems and alerts
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Initially, no alert history records should exist
initialHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.Zero(t, initialHistoryCount, "Should have 0 alert history records initially")
// Set system to up initially
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
// Set system to down to trigger alert
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
// Wait for alert to trigger (after the downtime delay)
// With 1 minute delay, we need to wait at least 1 minute + some buffer
time.Sleep(time.Second * 75)
// Check that alert is triggered
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Alert should be triggered")
// Check that alert history record was created
historyCount, err := hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for triggered alert")
// Get the alert history record and verify it's not resolved immediately
historyRecord, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should exist")
assert.Equal(t, alert.Id, historyRecord.GetString("alert_id"), "Alert history should reference correct alert")
assert.Equal(t, system.Id, historyRecord.GetString("system"), "Alert history should reference correct system")
assert.Equal(t, "Status", historyRecord.GetString("name"), "Alert history should have correct name")
// The alert history might be resolved immediately in some cases, so let's check the alert's triggered status
alertRecord, err := hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alert.Id})
assert.NoError(t, err)
assert.True(t, alertRecord.GetBool("triggered"), "Alert should still be triggered when checking history")
// Now resolve the alert by setting system back to up
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(200 * time.Millisecond)
// Check that alert is no longer triggered
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "Alert should not be triggered after system is back up")
// Check that alert history record is now resolved
historyRecord, err = hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should still exist")
assert.NotNil(t, historyRecord.Get("resolved"), "Alert history should be resolved")
// Test deleting a triggered alert resolves its history
// Create another system and alert
systems2, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system2 := systems2[0]
system2.Set("name", "test-system-2") // Rename for clarity
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
alert2, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system2.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Set system2 to down to trigger alert
system2.Set("status", "down")
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
// Wait for alert to trigger
time.Sleep(time.Second * 75)
// Verify alert is triggered and history record exists
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Second alert should be triggered")
historyCount, err = hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for second alert")
// Delete the triggered alert
err = hub.Delete(alert2)
assert.NoError(t, err)
// Check that alert history record is resolved after deletion
historyRecord2, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord2, "Alert history record should still exist after alert deletion")
assert.NotNil(t, historyRecord2.Get("resolved"), "Alert history should be resolved after alert deletion")
// Verify total history count is correct (2 records total)
totalHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.EqualValues(t, 2, totalHistoryCount, "Should have 2 total alert history records")
})
}

View File

@@ -0,0 +1,55 @@
package alerts
import (
"sync"
"time"
"github.com/pocketbase/pocketbase/core"
)
func (am *AlertManager) GetAlertManager() *AlertManager {
return am
}
func (am *AlertManager) GetPendingAlerts() *sync.Map {
return &am.pendingAlerts
}
func (am *AlertManager) GetPendingAlertsCount() int {
count := 0
am.pendingAlerts.Range(func(key, value any) bool {
count++
return true
})
return count
}
// ProcessPendingAlerts manually processes all expired alerts (for testing)
func (am *AlertManager) ProcessPendingAlerts() ([]*core.Record, error) {
now := time.Now()
var lastErr error
var processedAlerts []*core.Record
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
if now.After(info.expireTime) {
// Downtime delay has passed, process alert
if err := am.sendStatusAlert("down", info.systemName, info.alertRecord); err != nil {
lastErr = err
}
processedAlerts = append(processedAlerts, info.alertRecord)
am.pendingAlerts.Delete(key)
}
return true
})
return processedAlerts, lastErr
}
// ForceExpirePendingAlerts sets all pending alerts to expire immediately (for testing)
func (am *AlertManager) ForceExpirePendingAlerts() {
now := time.Now()
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
info.expireTime = now.Add(-time.Second) // Set to 1 second ago
return true
})
}

View File

@@ -1,14 +1,14 @@
package main
import (
"beszel"
"beszel/internal/agent"
"beszel/internal/agent/health"
"fmt"
"log"
"os"
"strings"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent"
"github.com/henrygd/beszel/agent/health"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh"
)

View File

@@ -1,12 +1,13 @@
package main
import (
"beszel/internal/agent"
"crypto/ed25519"
"os"
"path/filepath"
"testing"
"github.com/henrygd/beszel/agent"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@@ -1,15 +1,16 @@
package main
import (
"beszel"
"beszel/internal/hub"
_ "beszel/migrations"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/hub"
_ "github.com/henrygd/beszel/internal/migrations"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/spf13/cobra"

View File

@@ -2,15 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
# RUN go mod download
COPY *.go ./
COPY cmd ./cmd
COPY internal ./internal
COPY ../go.mod ../go.sum ./
RUN go mod download
# Copy source files
COPY . ./
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
RUN rm -rf /tmp/*

View File

@@ -3,16 +3,11 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app
# Download Go modules
COPY go.mod go.sum ./
COPY ../go.mod ../go.sum ./
RUN go mod download
# Copy source files
COPY *.go ./
COPY cmd ./cmd
COPY internal ./internal
COPY migrations ./migrations
COPY site/dist ./site/dist
COPY site/*.go ./site
COPY . ./
RUN apk add --no-cache \
unzip \
@@ -22,7 +17,7 @@ RUN update-ca-certificates
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./internal/cmd/hub
# ? -------------------------
FROM scratch

View File

@@ -3,8 +3,9 @@ package system
// TODO: this is confusing, make common package with common/types common/helpers etc
import (
"beszel/internal/entities/container"
"time"
"github.com/henrygd/beszel/internal/entities/container"
)
type Stats struct {

View File

@@ -4,7 +4,6 @@
package ghupdate
import (
"beszel"
"context"
"encoding/json"
"fmt"
@@ -16,6 +15,8 @@ import (
"runtime"
"strings"
"github.com/henrygd/beszel"
"github.com/blang/semver"
)

View File

@@ -1,9 +1,6 @@
package hub
import (
"beszel/internal/common"
"beszel/internal/hub/expirymap"
"beszel/internal/hub/ws"
"errors"
"net"
"net/http"
@@ -11,6 +8,10 @@ import (
"sync"
"time"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/hub/expirymap"
"github.com/henrygd/beszel/internal/hub/ws"
"github.com/blang/semver"
"github.com/lxzan/gws"
"github.com/pocketbase/dbx"

View File

@@ -4,9 +4,6 @@
package hub
import (
"beszel/internal/agent"
"beszel/internal/common"
"beszel/internal/hub/ws"
"crypto/ed25519"
"fmt"
"net/http"
@@ -17,6 +14,10 @@ import (
"testing"
"time"
"github.com/henrygd/beszel/agent"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/hub/ws"
"github.com/pocketbase/pocketbase/core"
pbtests "github.com/pocketbase/pocketbase/tests"
"github.com/stretchr/testify/assert"

View File

@@ -2,13 +2,14 @@
package config
import (
"beszel/internal/entities/system"
"fmt"
"log"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cast"

View File

@@ -4,12 +4,14 @@
package config_test
import (
"beszel/internal/hub/config"
"beszel/internal/tests"
"os"
"path/filepath"
"testing"
"github.com/henrygd/beszel/internal/tests"
"github.com/henrygd/beszel/internal/hub/config"
"github.com/pocketbase/pocketbase/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@@ -2,25 +2,23 @@
package hub
import (
"beszel"
"beszel/internal/alerts"
"beszel/internal/hub/config"
"beszel/internal/hub/systems"
"beszel/internal/records"
"beszel/internal/users"
"beszel/site"
"crypto/ed25519"
"encoding/pem"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/alerts"
"github.com/henrygd/beszel/internal/hub/config"
"github.com/henrygd/beszel/internal/hub/systems"
"github.com/henrygd/beszel/internal/records"
"github.com/henrygd/beszel/internal/users"
"github.com/google/uuid"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
@@ -71,6 +69,8 @@ func (h *Hub) StartHub() error {
if err := config.SyncSystems(e); err != nil {
return err
}
// register middlewares
h.registerMiddlewares(e)
// register api routes
if err := h.registerApiRoutes(e); err != nil {
return err
@@ -164,55 +164,6 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
return nil
}
// startServer sets up the server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// TODO: exclude dev server from production binary
switch h.IsDev() {
case true:
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
default:
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1)
indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, indexContent)
})
}
return nil
}
// registerCronJobs sets up scheduled tasks
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
// delete old system_stats and alerts_history records once every hour
@@ -222,6 +173,41 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
return nil
}
// custom middlewares
func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
// authenticate with trusted header
if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
se.Router.BindFunc(func(e *core.RequestEvent) error {
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()
})
}
}
// custom api routes
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
// auth protected routes

View File

@@ -4,9 +4,6 @@
package hub_test
import (
beszelTests "beszel/internal/tests"
"testing"
"bytes"
"crypto/ed25519"
"encoding/json"
@@ -16,6 +13,10 @@ import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/henrygd/beszel/internal/migrations"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/pocketbase/core"
pbTests "github.com/pocketbase/pocketbase/tests"
@@ -534,6 +535,115 @@ func TestApiRoutesAuthentication(t *testing.T) {
}
}
func TestFirstUserCreation(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenarios := []beszelTests.ApiScenario{
{
Name: "POST /create-user - should be available when no users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 200,
ExpectedContent: []string{"User created"},
TestAppFactory: testAppFactoryExisting,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.Zero(t, userCount, "Should start with no users")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser")
require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, userCount, "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should have created one superuser")
require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
},
{
Name: "POST /create-user - should not be available when users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactoryExisting,
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
})
t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) {
os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com")
os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123")
defer os.Unsetenv("BESZEL_HUB_USER_EMAIL")
defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactory := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenario := beszelTests.ApiScenario{
Name: "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should start with one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should still have one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should still have one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
}
scenario.Test(t)
})
}
func TestCreateUserEndpointAvailability(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
@@ -601,3 +711,63 @@ func TestCreateUserEndpointAvailability(t *testing.T) {
scenario.Test(t)
})
}
func TestTrustedHeaderMiddleware(t *testing.T) {
var hubs []*beszelTests.TestHub
defer func() {
defer os.Unsetenv("TRUSTED_AUTH_HEADER")
for _, hub := range hubs {
hub.Cleanup()
}
}()
os.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted")
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 trusted header should fail",
Method: http.MethodGet,
URL: "/api/beszel/getkey",
ExpectedStatus: 401,
ExpectedContent: []string{"requires valid"},
TestAppFactory: testAppFactory,
},
{
Name: "GET /getkey - with trusted header should fail if no matching user",
Method: http.MethodGet,
URL: "/api/beszel/getkey",
Headers: map[string]string{
"X-Beszel-Trusted": "user@test.com",
},
ExpectedStatus: 401,
ExpectedContent: []string{"requires valid"},
TestAppFactory: testAppFactory,
},
{
Name: "GET /getkey - with trusted header should succeed",
Method: http.MethodGet,
URL: "/api/beszel/getkey",
Headers: map[string]string{
"X-Beszel-Trusted": "user@test.com",
},
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)
}
}

View File

@@ -3,7 +3,7 @@
package hub
import "beszel/internal/hub/systems"
import "github.com/henrygd/beszel/internal/hub/systems"
// TESTING ONLY: GetSystemManager returns the system manager
func (h *Hub) GetSystemManager() *systems.SystemManager {

View File

@@ -0,0 +1,82 @@
//go:build development
package hub
import (
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/henrygd/beszel"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/osutils"
)
// Wraps http.RoundTripper to modify dev proxy HTML responses
type responseModifier struct {
transport http.RoundTripper
hub *Hub
}
func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := rm.transport.RoundTrip(req)
if err != nil {
return resp, err
}
// Only modify HTML responses
contentType := resp.Header.Get("Content-Type")
if !strings.Contains(contentType, "text/html") {
return resp, nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp, err
}
resp.Body.Close()
// Create a new response with the modified body
modifiedBody := rm.modifyHTML(string(body))
resp.Body = io.NopCloser(strings.NewReader(modifiedBody))
resp.ContentLength = int64(len(modifiedBody))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody)))
return resp, nil
}
func (rm *responseModifier) modifyHTML(html string) string {
parsedURL, err := url.Parse(rm.hub.appURL)
if err != nil {
return html
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
html = strings.ReplaceAll(html, "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1)
return html
}
// startServer sets up the development server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
slog.Info("starting server", "appURL", h.appURL)
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
proxy.Transport = &responseModifier{
transport: http.DefaultTransport,
hub: h,
}
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
_ = osutils.LaunchURL(h.appURL)
return nil
}

View File

@@ -0,0 +1,52 @@
//go:build !development
package hub
import (
"io/fs"
"net/http"
"net/url"
"strings"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/site"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
// startServer sets up the production server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
html := strings.ReplaceAll(string(indexFile), "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, html)
})
return nil
}

View File

@@ -1,9 +1,6 @@
package systems
import (
"beszel"
"beszel/internal/entities/system"
"beszel/internal/hub/ws"
"context"
"encoding/json"
"errors"
@@ -13,6 +10,12 @@ import (
"strings"
"time"
"github.com/henrygd/beszel/internal/hub/ws"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/henrygd/beszel"
"github.com/blang/semver"
"github.com/fxamacker/cbor/v2"
"github.com/pocketbase/pocketbase/core"

View File

@@ -1,14 +1,18 @@
package systems
import (
"beszel"
"beszel/internal/common"
"beszel/internal/entities/system"
"beszel/internal/hub/ws"
"errors"
"fmt"
"time"
"github.com/henrygd/beszel/internal/hub/ws"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel"
"github.com/blang/semver"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/store"
@@ -30,10 +34,8 @@ const (
sessionTimeout = 4 * time.Second
)
var (
// errSystemExists is returned when attempting to add a system that already exists
errSystemExists = errors.New("system exists")
)
// errSystemExists is returned when attempting to add a system that already exists
var errSystemExists = errors.New("system exists")
// SystemManager manages a collection of monitored systems and their connections.
// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.

View File

@@ -4,16 +4,17 @@
package systems_test
import (
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"beszel/internal/hub/systems"
"beszel/internal/tests"
"fmt"
"sync"
"testing"
"testing/synctest"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/henrygd/beszel/internal/hub/systems"
"github.com/henrygd/beszel/internal/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -4,9 +4,10 @@
package systems
import (
entities "beszel/internal/entities/system"
"context"
"fmt"
entities "github.com/henrygd/beszel/internal/entities/system"
)
// TESTING ONLY: GetSystemCount returns the number of systems in the store
@@ -100,3 +101,10 @@ func (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) boo
return true
}
// TESTING ONLY: RemoveAllSystems removes all systems from the store
func (sm *SystemManager) RemoveAllSystems() {
for _, system := range sm.systems.GetAll() {
sm.RemoveSystem(system.Id)
}
}

View File

@@ -1,12 +1,12 @@
package hub
import (
"beszel/internal/ghupdate"
"fmt"
"log"
"os"
"os/exec"
"github.com/henrygd/beszel/internal/ghupdate"
"github.com/spf13/cobra"
)

View File

@@ -1,12 +1,14 @@
package ws
import (
"beszel/internal/common"
"beszel/internal/entities/system"
"errors"
"time"
"weak"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/lxzan/gws"
"golang.org/x/crypto/ssh"

View File

@@ -4,11 +4,12 @@
package ws
import (
"beszel/internal/common"
"crypto/ed25519"
"testing"
"time"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@@ -0,0 +1,71 @@
package migrations
import (
"os"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
const (
TempAdminEmail = "_@b.b"
)
func init() {
m.Register(func(app core.App) error {
// initial settings
settings := app.Settings()
settings.Meta.AppName = "Beszel"
settings.Meta.HideControls = true
settings.Logs.MinLevel = 4
if err := app.Save(settings); err != nil {
return err
}
// create superuser
superuserCollection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
superUser := core.NewRecord(superuserCollection)
// set email
email, _ := GetEnv("USER_EMAIL")
password, _ := GetEnv("USER_PASSWORD")
didProvideUserDetails := email != "" && password != ""
// set superuser email
if email == "" {
email = TempAdminEmail
}
superUser.SetEmail(email)
// set superuser password
if password != "" {
superUser.SetPassword(password)
} else {
superUser.SetRandomPassword()
}
// if user details are provided, we create a regular user as well
if didProvideUserDetails {
usersCollection, _ := app.FindCollectionByNameOrId("users")
user := core.NewRecord(usersCollection)
user.SetEmail(email)
user.SetPassword(password)
user.SetVerified(true)
err := app.Save(user)
if err != nil {
return err
}
}
return app.Save(superUser)
}, nil)
}
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
func GetEnv(key string) (value string, exists bool) {
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
return value, exists
}
// Fallback to the old unprefixed key
return os.LookupEnv(key)
}

View File

@@ -2,8 +2,6 @@
package records
import (
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"encoding/json"
"fmt"
"log"
@@ -11,6 +9,9 @@ import (
"strings"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
@@ -39,12 +40,14 @@ type StatsRecord struct {
}
// global variables for reusing allocations
var statsRecord StatsRecord
var containerStats []container.Stats
var sumStats system.Stats
var tempStats system.Stats
var queryParams = make(dbx.Params, 1)
var containerSums = make(map[string]*container.Stats)
var (
statsRecord StatsRecord
containerStats []container.Stats
sumStats system.Stats
tempStats system.Stats
queryParams = make(dbx.Params, 1)
containerSums = make(map[string]*container.Stats)
)
// Create longer records by averaging shorter records
func (rm *RecordManager) CreateLongerRecords() {

View File

@@ -4,12 +4,13 @@
package records_test
import (
"beszel/internal/records"
"beszel/internal/tests"
"fmt"
"testing"
"time"
"github.com/henrygd/beszel/internal/records"
"github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"

38
internal/site/biome.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "es5"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

BIN
internal/site/bun.lockb Executable file

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.6",
"version": "0.12.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.6",
"version": "0.12.7",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -35,6 +35,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"d3-time": "^3.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.452.0",
"nanostores": "^0.11.4",
"pocketbase": "^0.26.2",
@@ -4273,6 +4274,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",

Some files were not shown because too many files have changed in this diff Show More