Compare commits

...

227 Commits

Author SHA1 Message Date
Louis Lam
74448d39e8 Update to 2.0.0-beta.2 2025-03-28 23:25:28 +08:00
mantak
049005d2d2 Add Lithuanian language code to dropdown (#5732) 2025-03-28 16:45:58 +08:00
Louis Lam
ad444cdbcf Translations Update from Weblate (#5669) 2025-03-28 16:42:39 +08:00
mantak
325bec42ee Translated using Weblate (Lithuanian)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Lithuanian)

Currently translated at 89.5% (957 of 1069 strings)

Translated using Weblate (Lithuanian)

Currently translated at 88.4% (945 of 1069 strings)

Co-authored-by: mantak <mantaksam@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/lt/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:29 +00:00
jolupa
95780656df Translated using Weblate (Catalan)
Currently translated at 52.1% (558 of 1069 strings)

Co-authored-by: jolupa <jolupameister@protonmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ca/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:29 +00:00
bioslet
45ab84e1f9 Translated using Weblate (Russian)
Currently translated at 98.5% (1053 of 1069 strings)

Co-authored-by: bioslet <bioslet@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:29 +00:00
Ramin Mazloomi
71d0e7c15a Translated using Weblate (Persian)
Currently translated at 98.7% (1056 of 1069 strings)

Co-authored-by: Ramin Mazloomi <RaminMT007@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Saba Sakvarelidze
5c68c2b5d9 Translated using Weblate (Georgian)
Currently translated at 1.2% (13 of 1067 strings)

Co-authored-by: Saba Sakvarelidze <cal1b4nnn@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ka/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
AnnAngela
c800638ea1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1067 of 1067 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
spectre011-code
fb3031edf0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.5% (1056 of 1061 strings)

Co-authored-by: spectre011-code <spectre011@protonmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Ivan Bratović
f46379b48a Translated using Weblate (Croatian)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (1061 of 1061 strings)

Co-authored-by: Ivan Bratović <ivanbratovic4@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hr/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
stanol
084617ff4d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1067 of 1067 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1061 of 1061 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Cyril59310
c5c21668a2 Translated using Weblate (French)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1067 of 1067 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1061 of 1061 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Marco
eb9fc9a68d Translated using Weblate (German)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1067 of 1067 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1067 of 1067 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1061 of 1061 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1061 of 1061 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
MrEddX
937177177e Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1069 of 1069 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1067 of 1067 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1061 of 1061 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (1055 of 1055 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
yasin
061406d7d1 Translated using Weblate (Turkish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: yasin <muratozbayyasin@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Melroy van den Berg
f563f49bf0 Translated using Weblate (Dutch)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Melroy van den Berg <melroy@melroy.org>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/nl/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
svalo
93bcfe76da Translated using Weblate (Italian)
Currently translated at 67.1% (707 of 1053 strings)

Co-authored-by: svalo <code@valo.space>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Tomáš Novák
35dfd5c684 Translated using Weblate (Slovak)
Currently translated at 55.1% (581 of 1053 strings)

Co-authored-by: Tomáš Novák <tomas@tonova.sk>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sk/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Black
dbec3fcc13 Translated using Weblate (Romanian)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Romanian)

Currently translated at 97.2% (1024 of 1053 strings)

Co-authored-by: Black <cristian.badea@yahoo.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ro/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Dominik727
b71c44efa0 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Dominik727 <suszterdominik@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Gil Obradors
b0d1d913bc Translated using Weblate (Catalan)
Currently translated at 52.5% (553 of 1053 strings)

Co-authored-by: Gil Obradors <gil.obradors@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ca/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Kisem
da8a697b51 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Kisem <kiss.m.aron@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Dominik727
bb7c0d9e09 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Dominik727 <suszterdominik@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Alex Nagy
db835a0309 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Alex Nagy <nagyalex003@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-03-28 08:34:28 +00:00
Louis Lam
44db422128 Update vite (#5731) 2025-03-28 16:34:23 +08:00
Teror Fox
30f82b9cb4 feat: add notification provider PushPlus (#5716)
Co-authored-by: Teror Fox <i@trfox.top>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-03-26 17:31:34 +01:00
Louis Lam
10a518e72e Fix #5721: Change proxy port column type to integer to support larger port numbers (#5724) 2025-03-25 02:07:15 +08:00
Louis Lam
a7d1b99719 Update dependencies (#5715) 2025-03-22 16:40:31 +08:00
Steven Stromberg
f50e26edd6 Update PWA icons to have transparency (#5714)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2025-03-22 08:05:21 +08:00
RaminMT
b0610c02ac Add Telegram custom bot api server support (#5668) 2025-03-21 04:08:20 +01:00
devlikeapro
6f8f8f955f Add WhatsApp (WAHA) notification provider (#5647)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-03-18 13:06:43 +01:00
Sergio Conde Gómez
9857770cc7 feat: rework notification templating and add support for telegram (#5637) 2025-03-14 12:51:07 +01:00
Harry
fce824f5a8 feat: Support YZJ notification provider (#5686)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-03-14 02:40:13 +01:00
Melroy van den Berg
cf44d39e1b Do not use --production flag anymore (#5644)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-03-07 23:55:02 +01:00
Fred Nnaji
230832c382 Update TLS expiration warning (#5659)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-03-07 21:49:38 +01:00
Louis Lam
c7d7fdd632 Update dependencies (#5664) 2025-02-28 01:32:23 +08:00
Louis Lam
0fc6998a31 Translations Update from Weblate (#5474) 2025-02-28 00:41:09 +08:00
hamx01
bfd4b84a19 Translated using Weblate (Russian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: hamx01 <asolianik2015@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
DJEDIVER
eef742fe93 Translated using Weblate (Russian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: DJEDIVER <djediver@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
nikito7
4307f7f79b Translated using Weblate (Portuguese (Portugal))
Currently translated at 44.4% (468 of 1053 strings)

Co-authored-by: nikito7 <rsansana@outlook.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_PT/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Fernando Pindado
1bf98aa114 Translated using Weblate (Spanish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Fernando Pindado <fpindado@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
aaf-30
d03de1fca4 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: aaf-30 <achmadalfandi53@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
csjoh
681530dc1a Translated using Weblate (Norwegian Bokmål)
Currently translated at 54.5% (574 of 1053 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 50.8% (535 of 1053 strings)

Co-authored-by: csjoh <github@csj.no>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/nb_NO/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
RainoutOvO
86362a802f Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Chinese (Traditional, Hong Kong))

Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 91.6% (965 of 1053 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: RainoutOvO <rainout@163.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Shadow Gaming
de8e42b748 Translated using Weblate (Portuguese)
Currently translated at 19.2% (203 of 1053 strings)

Co-authored-by: Shadow Gaming <workspace.shadowgaming@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
José Perlin
196c9cae57 Translated using Weblate (Portuguese)
Currently translated at 19.2% (203 of 1053 strings)

Co-authored-by: José Perlin <jose.vendr@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Nikolay (Mansas)
79ffb6e159 Translated using Weblate (Russian)
Currently translated at 98.3% (1036 of 1053 strings)

Co-authored-by: Nikolay (Mansas) <dokykin0nikolay@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Jonathan Neider Larsen
d8418a8949 Translated using Weblate (Danish)
Currently translated at 73.0% (769 of 1053 strings)

Co-authored-by: Jonathan Neider Larsen <jonathanneider@pm.me>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/da/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Vladislav Selyukov
e496f37b15 Translated using Weblate (Russian)
Currently translated at 98.0% (1032 of 1053 strings)

Co-authored-by: Vladislav Selyukov <vladxp1838@mail.ru>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Buchtič
457626c715 Translated using Weblate (Czech)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Buchtič <martin.buchta@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
TheSkout001
17f3d5214a Translated using Weblate (Russian)
Currently translated at 97.9% (1031 of 1053 strings)

Co-authored-by: TheSkout001 <daniil.afg@yandex.ru>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
nkgiovannivl
d7c76abff7 Translated using Weblate (Spanish)
Currently translated at 97.0% (1022 of 1053 strings)

Co-authored-by: nkgiovannivl <diego.vera.leon@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Alex Nagy
1dd8c16fb2 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.0% (1011 of 1053 strings)

Co-authored-by: Alex Nagy <nagyalex003@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Sfont
f932531320 Translated using Weblate (Catalan)
Currently translated at 21.5% (227 of 1053 strings)

Co-authored-by: Sfont <sfont@duck.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ca/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Kisem
cbb2ae7139 Translated using Weblate (Hungarian)
Currently translated at 89.0% (938 of 1053 strings)

Co-authored-by: Kisem <kiss.m.aron@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Alex Nagy
084efe3d17 Translated using Weblate (Hungarian)
Currently translated at 89.0% (938 of 1053 strings)

Co-authored-by: Alex Nagy <nagyalex003@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Kisem
db112d9c47 Translated using Weblate (Hungarian)
Currently translated at 80.8% (851 of 1053 strings)

Co-authored-by: Kisem <kiss.m.aron@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Alex Nagy
cec6b2e79a Translated using Weblate (Hungarian)
Currently translated at 80.8% (851 of 1053 strings)

Co-authored-by: Alex Nagy <nagyalex003@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Titas
a8ccc730b1 Translated using Weblate (Lithuanian)
Currently translated at 27.3% (288 of 1053 strings)

Co-authored-by: Titas <titasp@pixylt.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/lt/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Yuki
5bd3b02a4e Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 72.0% (759 of 1053 strings)

Co-authored-by: Yuki <903728862@qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
dongsu8142
24d76489a7 Translated using Weblate (Korean)
Currently translated at 72.0% (759 of 1053 strings)

Co-authored-by: dongsu8142 <dongsu8142@naver.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Arian Ardeshiri
9afac23029 Translated using Weblate (Persian)
Currently translated at 97.9% (1031 of 1053 strings)

Co-authored-by: Arian Ardeshiri <arian.fawiki@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Shadow Gaming
cef81ecc5a Translated using Weblate (Portuguese)
Currently translated at 9.5% (101 of 1053 strings)

Co-authored-by: Shadow Gaming <workspace.shadowgaming@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Tise
1e297e2867 Translated using Weblate (Russian)
Currently translated at 97.8% (1030 of 1053 strings)

Co-authored-by: Tise <vladden500@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Unai Tolosa Pontesta
efce9072f6 Translated using Weblate (Basque)
Currently translated at 62.8% (662 of 1053 strings)

Translated using Weblate (Basque)

Currently translated at 62.0% (653 of 1053 strings)

Translated using Weblate (Basque)

Currently translated at 61.9% (652 of 1053 strings)

Translated using Weblate (Basque)

Currently translated at 59.1% (623 of 1053 strings)

Co-authored-by: Unai Tolosa Pontesta <utolosa002@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/eu/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
İzzet Emre Demir
46b3bd314f Translated using Weblate (Turkish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: İzzet Emre Demir <izzetemredemir@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
mahdi
427cf2a7ac Translated using Weblate (Persian)
Currently translated at 96.4% (1016 of 1053 strings)

Co-authored-by: mahdi <mahdi.section8@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Aluisio
e5af814ab3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.9% (1000 of 1053 strings)

Co-authored-by: Aluisio <aluisiodeavila@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Stefano ACCIARRI
a0ab37554f Translated using Weblate (Italian)
Currently translated at 66.9% (705 of 1053 strings)

Co-authored-by: Stefano ACCIARRI <stefanoacciarri@bazweb.it>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Eliyas Hossain
468f7c2aeb Translated using Weblate (Bengali)
Currently translated at 21.1% (223 of 1053 strings)

Co-authored-by: Eliyas Hossain <eliyas.ru1991@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bn/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:53 +00:00
Celer21
ae202ae272 Translated using Weblate (Czech)
Currently translated at 98.3% (1036 of 1053 strings)

Co-authored-by: Celer21 <celer21@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Rabin
fa6f717a5e Added translation using Weblate (Nepali)
Co-authored-by: Rabin <gautamrabinraj@gmail.com>
2025-02-27 11:26:52 +00:00
Ryo Hanafusa
d57e1d315f Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Ryo Hanafusa <ryo7gumi@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
KEINOS
25b224ed19 Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: KEINOS <github@keinos.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Ryo Hanafusa
0e9020d0e3 Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Ryo Hanafusa <ryo7gumi@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
takumi0213
8b774da885 Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: takumi0213 <52845601+takumi0213@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
KEINOS
d14ccb0a14 Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: KEINOS <github@keinos.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Yoswaris Lawpaiboon
09f4ac57f2 Translated using Weblate (Thai)
Currently translated at 63.2% (666 of 1053 strings)

Co-authored-by: Yoswaris Lawpaiboon <konglha19@outlook.co.th>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/th/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Ryo Hanafusa
5c87b3c78a Translated using Weblate (Japanese)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Japanese)

Currently translated at 66.2% (698 of 1053 strings)

Co-authored-by: Ryo Hanafusa <ryo7gumi@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Youssef Bourourou
27041b337f Translated using Weblate (Arabic)
Currently translated at 69.4% (731 of 1053 strings)

Co-authored-by: Youssef Bourourou <bourourouyoussef1@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ar/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Pierre Cazenave
7d0f095bb4 Translated using Weblate (French)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Pierre Cazenave <pierre.cazenave33@outlook.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2025-02-27 11:26:52 +00:00
Melroy van den Berg
65e6f10820 Update to latest stable version on the main branch (#5645) 2025-02-27 19:26:46 +08:00
Zaid Hafeez
b45dc6787d Use GitHub's new "GitHub Hosted" Ubuntu ARM runner (#5587)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-02-16 21:06:19 +01:00
GJS
7482d5824d Fix: Updated security issue template (#5604) 2025-02-06 19:38:50 +01:00
GJS
44f5a896d0 Enhance security issue template (#5593) 2025-02-06 13:33:27 +01:00
希亚
784ac9ca35 Modify the "Time" in the AliyunSMS to local time. (#5582) 2025-02-01 00:12:09 +01:00
Boro Vukovic
20820f5a5a chore(deps): upgrade http/https/socks proxy agents (#5548)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-26 14:21:40 +01:00
zappityzap
66908c7055 chore(deps): update nostr notification provider (#5495)
Co-authored-by: zappityzap <zappityzap@proton.me>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-26 13:58:38 +01:00
DayShift
7a9191761d fix: make sure that stripping backslashes for notification urls cannot cause catastophic backtracking (ReDOS) (#5573)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-26 11:52:12 +01:00
Elliot Matson
7dc6191b0a fix: add notification-fallback for better google chat popups (#5476)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-24 18:49:29 +01:00
DayShift
03beef8006 BugFix:Regular Expression in parseDuration Function (#5563)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-22 19:03:38 +01:00
Louis Lam
223cde831f Fix push examples cannot be loaded (Docker only) (#5490) 2025-01-18 23:35:40 +08:00
Louis Lam
be2faf64ce Clarify that BSD OSs are not supported (#5465) 2024-12-22 13:46:13 +08:00
Louis Lam
0b76e19401 Clarify that BSD OSs are not supported 2024-12-22 13:45:19 +08:00
Ionys
97cbaac5d9 Fix monitor tags deletion (#5463) 2024-12-21 12:26:36 +01:00
Louis Lam
fd55659c2b Update to 2.0.0-beta.1 (#5460) 2024-12-20 16:47:00 +08:00
Louis Lam
20da4d29ab Update to 2.0.0-beta.1 2024-12-20 16:42:59 +08:00
Louis Lam
845c6e1189 Fix release script issue (#5459) 2024-12-20 16:42:20 +08:00
Louis Lam
46f771e9a2 Update dependencies (#5458) 2024-12-20 15:59:51 +08:00
Louis Lam
ccede11e1c 1.23.x changes to 2.0 (#5457) 2024-12-20 15:46:31 +08:00
Louis Lam
c872929b8a Translations Update from Weblate (#5282) 2024-12-20 15:40:27 +08:00
Louis Lam
4d16575599 Merge branch 'master' into 1.23.X-to-2
# Conflicts:
#	package-lock.json
#	package.json
2024-12-20 15:38:45 +08:00
Louis Lam
5bb329fa0e Update to 1.23.16 2024-12-20 15:15:52 +08:00
Louis Lam
09dedc07fb [1.23.X] Update dependencies (#5455) 2024-12-20 15:11:24 +08:00
Louis Lam
6cfae01a0d Merge commit from fork
* [V1 Only] Change dev server's data path to ./data/v1

* Fix GHSA-2qgm-m29m-cj2h
2024-12-20 15:02:22 +08:00
Amaury Gomes Ibanez
489b73a7a6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.5% (985 of 1053 strings)

Co-authored-by: Amaury Gomes Ibanez <amaury_mju@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:19 +00:00
vipnetant
1a7a738418 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: vipnetant <yao5434890@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:19 +00:00
DJEDIVER
4a25be875e Translated using Weblate (Russian)
Currently translated at 97.8% (1030 of 1053 strings)

Co-authored-by: DJEDIVER <djediver@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:19 +00:00
Youssef Bourourou
5edd22ffbe Translated using Weblate (Arabic)
Currently translated at 69.3% (730 of 1053 strings)

Co-authored-by: Youssef Bourourou <bourourouyoussef1@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ar/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:19 +00:00
JIAN YI CHEN
ba1031327e Translated using Weblate (Chinese (Traditional))
Currently translated at 89.7% (945 of 1053 strings)

Co-authored-by: JIAN YI CHEN <daniel@jianyi.tw>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
takumi0213
de141f1f26 Translated using Weblate (Japanese)
Currently translated at 65.9% (694 of 1053 strings)

Co-authored-by: takumi0213 <52845601+takumi0213@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Aindriú Mac Giolla Eoin
3114db18e9 Translated using Weblate (Irish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ga/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Mehedi Hasan
a6e46c6ed7 Translated using Weblate (Bengali)
Currently translated at 20.9% (221 of 1053 strings)

Co-authored-by: Mehedi Hasan <asmaparvin019@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bn/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Ilkka Myller
71a361c86c Translated using Weblate (Finnish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fi/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
joaorocha-ceiia
e5ed32c9cb Translated using Weblate (Portuguese (Portugal))
Currently translated at 44.3% (467 of 1053 strings)

Co-authored-by: joaorocha-ceiia <joao.rocha@ceiia.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_PT/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Aldair Guzman
340a5c8d5e Translated using Weblate (Spanish)
Currently translated at 96.1% (1012 of 1053 strings)

Co-authored-by: Aldair Guzman <aldairguzmant@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
CHAIYEON CHO
35f68decb2 Translated using Weblate (Korean)
Currently translated at 71.3% (751 of 1053 strings)

Co-authored-by: CHAIYEON CHO <stoutcho21@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Matěj Foltín
1417983b9c Translated using Weblate (Czech)
Currently translated at 97.0% (1022 of 1053 strings)

Co-authored-by: Matěj Foltín <foltinmatej@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Sezer Bozkir
9dec2cee26 Translated using Weblate (Turkish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Sezer Bozkir <sezer.tht@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Jordi Garcia
8631c8da07 Translated using Weblate (Catalan)
Currently translated at 21.3% (225 of 1053 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (1003 of 1053 strings)

Co-authored-by: Jordi Garcia <jgtorcal@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ca/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Michal
6aa09fb76a Translated using Weblate (Czech)
Currently translated at 96.5% (1017 of 1053 strings)

Co-authored-by: Michal <black23@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Ivan Bratović
a89e1735b8 Translated using Weblate (Croatian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Ivan Bratović <ivanbratovic4@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hr/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
stanol
d4e51fe557 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Adam Stachowicz
7a710b4ab7 Translated using Weblate (Polish)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Dominik727
1dfbce3961 Translated using Weblate (Hungarian)
Currently translated at 77.5% (817 of 1053 strings)

Co-authored-by: Dominik727 <suszterdominik@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hu/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Cyril59310
2f5453017e Translated using Weblate (French)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
MrEddX
14df207d55 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2024-12-20 06:55:18 +00:00
Ionys
2ecd7af89b feat(monitor condition): Remove the empty default condition creation (#5354) 2024-12-20 14:55:13 +08:00
Louis Lam
efdffca06c Fix: [MariaDB] Unable to clean up heartbeat data (#5425)
Co-authored-by: GJS <homelab.api@gmail.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-12-12 14:49:01 +08:00
Cyril59310
c0fe669cd8 Fix the save bar of the maintenance menu (#5385)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-11-27 22:09:07 +01:00
Luke Thomas
cdb8ad321d fix: slug input (#5239)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-11-26 01:56:23 +01:00
Ionys
4228dd0a29 Fix the timeout value (#5352) 2024-11-19 01:29:35 +01:00
Ionys
8a432ac937 fix(status page): Make sure the group deletion is correctly handled when groupIDList is empty (#5340) 2024-11-12 19:00:09 +01:00
Dorence Deng
778363a948 fix(notification): #5323 fix EditMonitor.vue useToast error (#5333) 2024-11-11 11:08:43 +01:00
Abhigyan Mohanta
6899603eb7 fix #5314 Email Address Friendly Name Error (#5318) 2024-11-09 23:30:08 +08:00
Daan Meijer
13ea190298 only allow for valid urls in slack button (#5312) 2024-11-05 20:50:47 +01:00
Frank Elsinga
a7407a1b65 fix not setting a default for json monitor operations (#5295) 2024-11-05 20:35:42 +08:00
Louis Lam
5bcde56a0f Publish docker images to ghcr.io (#5311) 2024-11-05 20:26:26 +08:00
Louis Lam
5864c6dd88 Fix: Embedded MariaDB startup issue (#5283) 2024-11-05 20:25:08 +08:00
Ionys
595b35fb15 fix(tags): Fix the tag deletion (#5298) 2024-11-02 13:09:23 +01:00
Louis Lam
0254e72177 Fix: Upload Artifact issue (#5271) 2024-10-29 22:10:05 +08:00
Louis Lam
06a272c119 Update to 2.0.0-beta.0 2024-10-29 21:20:32 +08:00
Louis Lam
93cf63cb06 Fix release script 2024-10-29 21:20:32 +08:00
Louis Lam
03bdaf70ca Translations Update from Weblate (#5198) 2024-10-29 19:31:02 +08:00
Harry Suryapambagya
65663e201b Translated using Weblate (Indonesian)
Currently translated at 100.0% (1053 of 1053 strings)

Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Shubham Mishra
351a48c6a4 Translated using Weblate (Hindi)
Currently translated at 10.2% (108 of 1052 strings)

Co-authored-by: Shubham Mishra <shubhammishralpu@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Ömer Faruk Genç
88b312c882 Translated using Weblate (Turkish)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
netzencatura
186f150cba Translated using Weblate (Czech)
Currently translated at 96.0% (1020 of 1062 strings)

Co-authored-by: netzencatura <info@netzen.cz>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Cyril59310
121b235056 Translated using Weblate (French)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Ilkka Myller
3df37151d8 Translated using Weblate (Finnish)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
AnnAngela
d6d13936f7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
stanol
6786466b65 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Ivan Bratović
7ab7000253 Translated using Weblate (Croatian)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: Ivan Bratović <ivanbratovic4@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Marco
1a75267138 Translated using Weblate (German)
Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1053 of 1053 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1062 of 1062 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
MrEddX
fea02d0512 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1062 of 1062 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2024-10-29 11:19:44 +00:00
Louis Lam
c79a3ef794 Drop exe source code (#5268) 2024-10-29 19:19:38 +08:00
Louis Lam
fed6e4bdb3 Improve the build process (#5267) 2024-10-29 19:02:49 +08:00
Ryo Hanafusa
e99fbf1ab7 docs: add slack username helptext (#5259)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-29 00:29:47 +01:00
Louis Lam
be6e5211e5 Revert #5152 (#5264) 2024-10-28 15:21:33 +08:00
Louis Lam
5ee986c58e Check knex filenames and rename json-yaml-validate.yml → validate.yml for general purposes (#5263) 2024-10-28 14:20:29 +08:00
Louis Lam
ca094296f2 Fix: Knex cannot set a default value for TEXT field (MariaDB) (#5261) 2024-10-28 13:16:22 +08:00
Louis Lam
277d6fe0ce Fix #5087 and fix migration state (#5260) 2024-10-27 20:30:44 +08:00
Louis Lam
46d8744fa4 Fix: Docker Healthcheck is not happy during migration (#5258) 2024-10-27 13:22:23 +08:00
Louis Lam
7d8dc55dbe Fix: the rootless user put in the wrong place (#5257) 2024-10-27 11:47:30 +08:00
Louis Lam
459fb138f2 Add next and next-slim tags (#5253) 2024-10-26 22:12:55 +08:00
Louis Lam
8f950a5145 Update dependencies (#5252) 2024-10-26 21:53:53 +08:00
Louis Lam
4d779cfc69 Data migration and history retention for 2.0.0 (#5075) 2024-10-26 20:50:29 +08:00
Louis Lam
2470451f6d Fix Apprise download issue (#5251) 2024-10-26 20:47:38 +08:00
Louis Lam
79a26180af Verify language json files format (#5233) 2024-10-23 12:47:04 +08:00
Louis Lam
7a82ae039c Fix weblate conflict and new translations (#5232)
Co-authored-by: Jochem Pluim <jochem@pluim.nu>
Co-authored-by: Zandor Smith <info@zsinfo.nl>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ryo Hanafusa <ryo7gumi@gmail.com>
Co-authored-by: Eduard Dev <legocuedy09@gmail.com>
Co-authored-by: Gunnar Norin <gunnar.norin@gmail.com>
Co-authored-by: AmadeusGraves <angelfx19@gmail.com>
Co-authored-by: Dan Misener <dan@misener.org>
Co-authored-by: Алексей Добрый <support@diera.ru>
Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Co-authored-by: Nelson Chan <chakflying@hotmail.com>
Co-authored-by: Lance <2124757129@qq.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: PhongPham <pttphong1202@gmail.com>
2024-10-23 08:36:22 +08:00
Easy
d2f71d11d6 Update API URL to compatible with the latest version of ServerChan (#5227) 2024-10-22 10:48:51 +02:00
Suven-p
c01494ec33 feat: add RabbitMQ monitor (#5199)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-20 15:57:03 +02:00
Ryo Hanafusa
a7e9bdd43e fix: expand hover trigger area of beat (#5223)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-20 15:30:03 +02:00
sctnightcore
d7ffa33950 feat: add notification provider SendGrid (#5205)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-18 02:25:23 +02:00
Frank Elsinga
a20a43b8aa tests: add testcase for the mqtt monitor with testcontainers (#4451) 2024-10-17 16:25:45 +02:00
Frank Elsinga
ed6087e233 fix: slack rich notification not working correctly (#5209) 2024-10-16 16:50:41 +02:00
Frank Elsinga
42e77798e5 Revert "Extend length of status bars in status page" (#5207) 2024-10-16 14:58:44 +02:00
Peace
85dfe1f5d1 fix: some errors from the performance optimization (#5196) 2024-10-16 13:25:35 +02:00
Louis Lam
b719d11500 Merge branch 'master' into fix-weblate-conflict2
# Conflicts:
#	src/lang/bg-BG.json
#	src/lang/cs-CZ.json
#	src/lang/de-CH.json
#	src/lang/de-DE.json
#	src/lang/fr-FR.json
#	src/lang/ga.json
#	src/lang/id-ID.json
#	src/lang/tr-TR.json
#	src/lang/uk-UA.json
#	src/lang/zh-CN.json
2024-10-15 00:56:41 +08:00
Ömer Faruk Genç
b7d2cedf2e Translated using Weblate (Turkish)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
2024-10-14 15:53:13 +00:00
Buchtič
f3ee9c2cad Translated using Weblate (Czech)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
2024-10-14 15:53:13 +00:00
Mohit Nagaraj
582fb6c5ad Added translation using Weblate (English (Middle)) 2024-10-12 03:23:55 +00:00
Erlaan
dda40610c7 feat: new notification provider 46elks (#5184)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-11 11:07:34 +02:00
Arjun Komath
bafca6bd37 feat: support sound and title in TechulusPush (#5178) 2024-10-11 00:19:58 +02:00
Jason Michael
bbc75b840b docs: grammatical improvements in contribution guide (#5183)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-10 08:57:59 +02:00
Dj Isaac
019702f8e5 fix: misspelling of 'address' (#5180) 2024-10-10 01:24:32 +02:00
YangHongYu
365aa8d814 Added translation using Weblate (Abkhazian) 2024-10-09 13:42:02 +00:00
YangHongYu
7e37dacb9a Added translation using Weblate (Uyghur) 2024-10-09 13:40:32 +00:00
Tanmay Shinde
e8c650797c refactor: removed unnecessary todo comment, modified jsdoc of utility func (#5164) 2024-10-09 09:47:00 +02:00
AnnAngela
958f96f06d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
2024-10-07 08:42:10 +00:00
Txopi
130d8d0177 Translated using Weblate (Basque)
Currently translated at 54.8% (557 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/eu/
2024-10-06 00:27:20 +00:00
Aindriú Mac Giolla Eoin
9905ca574c Translated using Weblate (Irish)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ga/
2024-10-05 14:27:30 +00:00
janinainfa
fc429108ac Translated using Weblate (Polish)
Currently translated at 94.2% (958 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
2024-10-05 14:27:30 +00:00
Cyril59310
10ffde2595 Translated using Weblate (French)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
2024-10-05 14:27:30 +00:00
stanol
b9b48e1b2d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
2024-10-04 11:50:37 +00:00
Harry Suryapambagya
a760898281 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
2024-10-04 11:50:37 +00:00
Marco
289b72d07d Translated using Weblate (German)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
2024-10-04 11:50:37 +00:00
Marco
9257a7a19e Translated using Weblate (German (Switzerland))
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
2024-10-04 11:50:37 +00:00
MrEddX
8084c7e61c Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1016 of 1016 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
2024-10-04 11:50:31 +00:00
Aung ko
324d879aad Translated using Weblate (Burmese)
Currently translated at 7.3% (75 of 1016 strings)

Translated using Weblate (English)

Currently translated at 99.9% (1015 of 1016 strings)

Co-authored-by: Aung ko <aungko.use@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/en/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/my/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Satveek Gupta
d2a44487b3 Translated using Weblate (Hindi)
Currently translated at 5.9% (60 of 1016 strings)

Co-authored-by: Satveek Gupta <guptasatveek@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ömer Faruk GÖL
35668219ec Translated using Weblate (Turkish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Ömer Faruk GÖL <omerfarukgol@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Lucius Gaitán
895f6d2ff1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.2% (978 of 1016 strings)

Co-authored-by: Lucius Gaitán <lucius@luciusgaitan.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Shinwoo PARK
3088cc6141 Translated using Weblate (Korean)
Currently translated at 73.3% (745 of 1016 strings)

Co-authored-by: Shinwoo PARK <devpysweb@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Mohammad Hesam Imani
98415bd419 Translated using Weblate (Persian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Mohammad Hesam Imani <hesamimany@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
AlanFarley
087d20b775 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.9% (965 of 1016 strings)

Co-authored-by: AlanFarley <alanfarley2010@live.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ivan M
ec7923f4fd Translated using Weblate (Belarusian)
Currently translated at 91.8% (933 of 1016 strings)

Co-authored-by: Ivan M <ivan.meishutovich@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/be/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
B3CKDOOR
86b3ff6bfd Translated using Weblate (Dutch)
Currently translated at 99.0% (1006 of 1016 strings)

Co-authored-by: B3CKDOOR <B3CKDOOR@users.noreply.weblate.kuma.pet>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/nl/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Elia Ronchetti
e6159d9ab4 Translated using Weblate (Italian)
Currently translated at 69.3% (705 of 1016 strings)

Co-authored-by: Elia Ronchetti <e.ronchetti@campus.unimib.it>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ossie Stinga
e781325633 Translated using Weblate (Italian)
Currently translated at 69.3% (705 of 1016 strings)

Co-authored-by: Ossie Stinga <ossie.stinga@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Sajjad Jazini
72478090e7 Translated using Weblate (Persian)
Currently translated at 98.1% (997 of 1016 strings)

Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
MrEddX
50ec9fec05 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Aindriú Mac Giolla Eoin
62c55f0e25 Translated using Weblate (Irish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ga/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Hyeonho Kang
3ab35c38fc Translated using Weblate (Korean)
Currently translated at 72.9% (741 of 1016 strings)

Co-authored-by: Hyeonho Kang <hyeonhokang10@o365.ice.go.kr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
stanol
1d86fa2b5c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
kindercat
cef072cae9 Translated using Weblate (Romanian)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (Romanian)

Currently translated at 95.5% (971 of 1016 strings)

Co-authored-by: kindercat <156439718+kindercat@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ro/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
AnnAngela
abcc98c836 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Lee Min Waan
aa38344c3d Translated using Weblate (Vietnamese)
Currently translated at 51.8% (527 of 1016 strings)

Co-authored-by: Lee Min Waan <rakis9076@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/vi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Harry Suryapambagya
9ff0ae67df Translated using Weblate (Indonesian)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.1% (967 of 1016 strings)

Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Michal
a14d05daab Translated using Weblate (Czech)
Currently translated at 96.1% (977 of 1016 strings)

Co-authored-by: Michal <black23@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Ilkka Myller
93c5ab0bd8 Translated using Weblate (Finnish)
Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fi/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Marco
d27a9e7d7f Translated using Weblate (German)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Cyril59310
c120c37030 Translated using Weblate (French)
Currently translated at 100.0% (1016 of 1016 strings)

Translated using Weblate (French)

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Louis Lam
acd1e7211a Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 70.9% (721 of 1016 strings)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2024-10-04 00:54:34 +00:00
Louis Lam
32dc76a085 Update to 1.23.15 2024-09-30 05:44:32 +08:00
Louis Lam
c6d6061a9f Pin cheerio to avoid the breaking change of undici (#5142) 2024-09-30 05:41:31 +08:00
168 changed files with 12008 additions and 5995 deletions

View File

@@ -32,7 +32,6 @@ tsconfig.json
/extra/healthcheck.exe
/extra/healthcheck
/extra/exe-builder
/extra/push-examples
/extra/uptime-kuma-push
# Comment the following line if you want to rebuild the healthcheck binary

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
---
blank_issues_enabled: false

View File

@@ -1,17 +0,0 @@
---
name: "Security Issue"
about: "Just for alerting @louislam, do not provide any details here"
title: "Security Issue"
ref: "main"
labels:
- security
---
DO NOT PROVIDE ANY DETAILS HERE. Please privately report to https://github.com/louislam/uptime-kuma/security/advisories/new.
Why need this issue? It is because GitHub Advisory do not send a notification to @louislam, it is a workaround to do so.
Your GitHub Advisory URL:

View File

@@ -0,0 +1,45 @@
---
name: "🛡️ Security Issue"
description: |
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
# title: "Security Issue"
labels: [security]
assignees: [louislam]
body:
- type: "markdown"
attributes:
value: |
## **⚠️ Report a Security Vulnerability**
### **IMPORTANT: DO NOT SHARE VULNERABILITY DETAILS HERE**
If you have discovered a security vulnerability, please report it securely using the GitHub Security Advisory.
**Note**: This issue is only for notifying the maintainers of the repository, as the GitHub Security Advisory does not automatically send notifications.
- **Confidentiality**: The information you provide in the GitHub Security Advisory will initially remain confidential. However, once the vulnerability is addressed, the advisory will be publicly disclosed on GitHub.
- **Access and Visibility**: Until the advisory is published, it will only be visible to the maintainers of the repository and invited collaborators.
- **Credit**: You will be automatically credited as a contributor for identifying and reporting the vulnerability. Your contribution will be reflected in the MITRE Credit System.
- **Important Reminder**: **Do not include any sensitive or detailed vulnerability information in this issue.** This issue is only for sharing the advisory URL to notify the maintainers of the repository, not for discussing the vulnerability itself.
**Thank you for helping us keep Uptime Kuma secure!**
## **Step 1: Submit a GitHub Security Advisory**
Right-click the link below and select `Open link in new tab` to access the page. This will keep the security issue open, allowing you to easily return and paste the Advisory URL here later.
➡️ [Create a New Security Advisory](https://github.com/louislam/uptime-kuma/security/advisories/new)
## **Step 2: Share the Advisory URL**
Once you've created your advisory, please share the URL below. This will notify Louis Lam and enable them to take the appropriate action.
- type: "textarea"
id: github-advisory-url
validations:
required: true
attributes:
label: "GitHub Advisory URL for @louislam"
placeholder: |
Please paste the GitHub Advisory URL here. Only the URL is required.
Example: https://github.com/louislam/uptime-kuma/security/advisories/GHSA-8h5r-7t6l-q3kz

View File

@@ -78,7 +78,7 @@ jobs:
e2e-test:
needs: [ ]
runs-on: ARM64
runs-on: ubuntu-24.04-arm
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4

View File

@@ -1,4 +1,4 @@
name: json-yaml-validate
name: validate
on:
push:
branches:
@@ -25,3 +25,19 @@ jobs:
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
# General validations
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Validate language JSON files
run: node ./extra/check-lang-json.js
- name: Validate knex migrations filename
run: node ./extra/check-knex-filenames.mjs

View File

@@ -1,6 +1,6 @@
# Project Info
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
First of all, I want to thank everyone who has submitted issues or shared pull requests for Uptime Kuma.
I never thought the GitHub community would be so nice!
Because of this, I also never thought that other people would actually read and edit my code.
Parts of the code are not very well-structured or commented, sorry about that.
@@ -9,7 +9,7 @@ The project was created with `vite.js` and is written in `vue3`.
Our backend lives in the `server`-directory and mostly communicates via websockets.
Both frontend and backend share the same `package.json`.
For production, the frontend is build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For production, the frontend is built into the `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For development, we run vite in development mode on another port.
## Directories
@@ -28,7 +28,7 @@ For development, we run vite in development mode on another port.
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do.
Both your and our maintainers time is precious, and we don't want to waste both time.
Both yours and our maintainers' time is precious, and we don't want to waste either.
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
@@ -49,11 +49,11 @@ Different guidelines exist for different types of pull requests (PRs):
<p>
If you come across a bug and think you can solve, we appreciate your work.
Please make sure that you follow by these rules:
Please make sure that you follow these rules:
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
- test that your code does what you came it does.
- test that your code does what you claim it does.
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
@@ -68,7 +68,7 @@ Different guidelines exist for different types of pull requests (PRs):
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new notification providers</b></summary>
@@ -102,7 +102,7 @@ Different guidelines exist for different types of pull requests (PRs):
Therefore, making sure that they work is also really important.
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
To make shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
To make sure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
- `UP`/`DOWN`
- Certificate Expiry via https://expired.badssl.com/
- Testing (the test button on the notification provider setup page)
@@ -117,7 +117,7 @@ Different guidelines exist for different types of pull requests (PRs):
| Testing | paste-image-here | paste-image-here |
```
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new monitoring types</b></summary>
@@ -138,14 +138,14 @@ Different guidelines exist for different types of pull requests (PRs):
-
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
<p>
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
This is especially important for a large pull request or you don't know if it will be merged or not.
This is especially important for a large pull request or when you don't know if it will be merged or not.
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
</p>
@@ -201,7 +201,7 @@ The rationale behind this is that we can align the direction and scope of the fe
## Project Styles
I personally do not like something that requires so many configurations before you can finally start the app.
I personally do not like something that requires a lot of configuration before you can finally start the app.
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
- Easy to install for non-Docker users
@@ -260,7 +260,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
```bash
npm run start-frontend-dev
@@ -409,7 +409,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### What is a maintainer and what are their roles?
This project has multiple maintainers which specialise in different areas.
This project has multiple maintainers who specialise in different areas.
Currently, there are 3 maintainers:
| Person | Role | Main Area |

View File

@@ -62,6 +62,7 @@ Requirements:
- Platform
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ FreeBSD / OpenBSD / NetBSD
- ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) 9

View File

@@ -0,0 +1,17 @@
exports.up = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.text("rabbitmq_nodes");
table.string("rabbitmq_username");
table.string("rabbitmq_password");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("rabbitmq_nodes");
table.dropColumn("rabbitmq_username");
table.dropColumn("rabbitmq_password");
});
};

View File

@@ -0,0 +1,7 @@
exports.up = function (knex) {
return knex("monitor").whereNull("json_path_operator").update("json_path_operator", "==");
};
exports.down = function (knex) {
// changing the json_path_operator back to null for all "==" is not possible anymore
// we have lost the context which fields have been set explicitely in >= v2.0 and which would need to be reverted
};

View File

@@ -0,0 +1,13 @@
// Update info_json column to LONGTEXT mainly for MariaDB
exports.up = function (knex) {
return knex.schema
.alterTable("monitor_tls_info", function (table) {
table.text("info_json", "longtext").alter();
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor_tls_info", function (table) {
table.text("info_json", "text").alter();
});
};

View File

@@ -0,0 +1,13 @@
// Fix #5721: Change proxy port column type to integer to support larger port numbers
exports.up = function (knex) {
return knex.schema
.alterTable("proxy", function (table) {
table.integer("port").alter();
});
};
exports.down = function (knex) {
return knex.schema.alterTable("proxy", function (table) {
table.smallint("port").alter();
});
};

View File

@@ -1,3 +1,13 @@
# Download Apprise deb package
FROM node:20-bookworm-slim AS download-apprise
WORKDIR /app
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
RUN apt update && \
apt --yes --no-install-recommends install curl && \
npm install cheerio semver && \
node ./download-apprise.mjs
# Base Image (Slim)
# If the image changed, the second stage image should be changed too
FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM
@@ -27,8 +37,9 @@ RUN apt update && \
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
apt update && \
# TODO: no idea how to delete the deb file after installation as it becomes a layer already
COPY --from=download-apprise /app/apprise.deb ./apprise.deb
RUN apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \

View File

@@ -27,7 +27,6 @@ RUN mkdir ./data
# ⭐ Main Image
############################################
FROM $BASE_IMAGE AS release
USER node
WORKDIR /app
LABEL org.opencontainers.image.source="https://github.com/louislam/uptime-kuma"
@@ -46,6 +45,7 @@ CMD ["node", "server/server.js"]
# Rootless Image
############################################
FROM release AS rootless
USER node
############################################
# Mark as Nightly

View File

@@ -5,7 +5,7 @@ const util = require("../../src/util");
util.polyfill();
const version = process.env.VERSION;
const version = process.env.RELEASE_BETA_VERSION;
console.log("Beta Version: " + version);

View File

@@ -0,0 +1,72 @@
import fs from "fs";
const dir = "./db/knex_migrations";
// Get the file list (ending with .js) from the directory
const files = fs.readdirSync(dir).filter((file) => file !== "README.md");
// They are wrong, but they had been merged, so allowed.
const exceptionList = [
"2024-08-24-000-add-cache-bust.js",
"2024-10-1315-rabbitmq-monitor.js",
];
// Correct format: YYYY-MM-DD-HHmm-description.js
for (const file of files) {
if (exceptionList.includes(file)) {
continue;
}
// Check ending with .js
if (!file.endsWith(".js")) {
console.error(`It should end with .js: ${file}`);
process.exit(1);
}
const parts = file.split("-");
// Should be at least 5 parts
if (parts.length < 5) {
console.error(`Invalid format: ${file}`);
process.exit(1);
}
// First part should be a year >= 2024
const year = parseInt(parts[0], 10);
if (isNaN(year) || year < 2023) {
console.error(`Invalid year: ${file}`);
process.exit(1);
}
// Second part should be a month
const month = parseInt(parts[1], 10);
if (isNaN(month) || month < 1 || month > 12) {
console.error(`Invalid month: ${file}`);
process.exit(1);
}
// Third part should be a day
const day = parseInt(parts[2], 10);
if (isNaN(day) || day < 1 || day > 31) {
console.error(`Invalid day: ${file}`);
process.exit(1);
}
// Fourth part should be HHmm
const time = parts[3];
// Check length is 4
if (time.length !== 4) {
console.error(`Invalid time: ${file}`);
process.exit(1);
}
const hour = parseInt(time.substring(0, 2), 10);
const minute = parseInt(time.substring(2), 10);
if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) {
console.error(`Invalid time: ${file}`);
process.exit(1);
}
}
console.log("All knex filenames are correct.");

27
extra/check-lang-json.js Normal file
View File

@@ -0,0 +1,27 @@
// For #5231
const fs = require("fs");
let path = "./src/lang";
// list directories in the lang directory
let jsonFileList = fs.readdirSync(path);
for (let jsonFile of jsonFileList) {
if (!jsonFile.endsWith(".json")) {
continue;
}
let jsonPath = path + "/" + jsonFile;
let originalContent = fs.readFileSync(jsonPath, "utf8");
let langData = JSON.parse(originalContent);
let formattedContent = JSON.stringify(langData, null, 4) + "\n";
if (originalContent !== formattedContent) {
console.error(`File ${jsonFile} is not formatted correctly.`);
process.exit(1);
}
}
console.log("All lang json files are formatted correctly.");

View File

@@ -0,0 +1,57 @@
// Go to http://ftp.debian.org/debian/pool/main/a/apprise/ using fetch api, where it is a apache directory listing page
// Use cheerio to parse the html and get the latest version of Apprise
// call curl to download the latest version of Apprise
// Target file: the latest version of Apprise, which the format is apprise_{VERSION}_all.deb
import * as cheerio from "cheerio";
import semver from "semver";
import * as childProcess from "child_process";
const baseURL = "http://ftp.debian.org/debian/pool/main/a/apprise/";
const response = await fetch(baseURL);
if (!response.ok) {
throw new Error("Failed to fetch page of Apprise Debian repository.");
}
const html = await response.text();
const $ = cheerio.load(html);
// Get all the links in the page
const linkElements = $("a");
// Filter the links which match apprise_{VERSION}_all.deb
const links = [];
const pattern = /apprise_(.*?)_all.deb/;
for (let i = 0; i < linkElements.length; i++) {
const link = linkElements[i];
if (link.attribs.href.match(pattern) && !link.attribs.href.includes("~")) {
links.push({
filename: link.attribs.href,
version: link.attribs.href.match(pattern)[1],
});
}
}
console.log(links);
// semver compare and download
let latestLink = {
filename: "",
version: "0.0.0",
};
for (const link of links) {
if (semver.gt(link.version, latestLink.version)) {
latestLink = link;
}
}
const downloadURL = baseURL + latestLink.filename;
console.log(`Downloading ${downloadURL}...`);
let result = childProcess.spawnSync("curl", [ downloadURL, "--output", "apprise.deb" ]);
console.log(result.stdout?.toString());
console.error(result.stderr?.toString());
process.exit(result.status !== null ? result.status : 1);

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env node
const childProcess = require("child_process");
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

View File

@@ -1 +0,0 @@
packages/

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.1" newVersion="4.1.1.1" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -1,84 +0,0 @@
using System.ComponentModel;
namespace UptimeKuma {
partial class DownloadForm {
/// <summary>
/// Required designer variable.
/// </summary>
private IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm));
this.progressBar = new System.Windows.Forms.ProgressBar();
this.label = new System.Windows.Forms.Label();
this.labelData = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// progressBar
//
this.progressBar.Location = new System.Drawing.Point(12, 12);
this.progressBar.Name = "progressBar";
this.progressBar.Size = new System.Drawing.Size(472, 41);
this.progressBar.TabIndex = 0;
//
// label
//
this.label.Location = new System.Drawing.Point(12, 59);
this.label.Name = "label";
this.label.Size = new System.Drawing.Size(472, 23);
this.label.TabIndex = 1;
this.label.Text = "Preparing...";
//
// labelData
//
this.labelData.Location = new System.Drawing.Point(12, 82);
this.labelData.Name = "labelData";
this.labelData.Size = new System.Drawing.Size(472, 23);
this.labelData.TabIndex = 2;
//
// DownloadForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(496, 117);
this.Controls.Add(this.labelData);
this.Controls.Add(this.label);
this.Controls.Add(this.progressBar);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "DownloadForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Uptime Kuma";
this.Load += new System.EventHandler(this.DownloadForm_Load);
this.ResumeLayout(false);
}
private System.Windows.Forms.Label labelData;
private System.Windows.Forms.Label label;
private System.Windows.Forms.ProgressBar progressBar;
#endregion
}
}

View File

@@ -1,204 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace UptimeKuma {
public partial class DownloadForm : Form {
private readonly Queue<DownloadItem> downloadQueue = new();
private readonly WebClient webClient = new();
private DownloadItem currentDownloadItem;
public DownloadForm() {
InitializeComponent();
}
private void DownloadForm_Load(object sender, EventArgs e) {
webClient.DownloadProgressChanged += DownloadProgressChanged;
webClient.DownloadFileCompleted += DownloadFileCompleted;
label.Text = "Reading latest version...";
// Read json from https://uptime.kuma.pet/version
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
var versionObj = JsonConvert.DeserializeObject<Version>(versionJson);
var nodeVersion = versionObj.nodejs;
var uptimeKumaVersion = versionObj.latest;
var hasUpdateFile = File.Exists("update");
if (!Directory.Exists("node")) {
downloadQueue.Enqueue(new DownloadItem {
URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip",
Filename = "node.zip",
TargetFolder = "node"
});
}
if (!Directory.Exists("core") || hasUpdateFile) {
// It is update, rename the core folder to core.old
if (Directory.Exists("core")) {
// Remove the old core.old folder
if (Directory.Exists("core.old")) {
Directory.Delete("core.old", true);
}
Directory.Move("core", "core.old");
}
downloadQueue.Enqueue(new DownloadItem {
URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip",
Filename = "core.zip",
TargetFolder = "core"
});
File.WriteAllText("version.json", versionJson);
// Delete the update file
if (hasUpdateFile) {
File.Delete("update");
}
}
DownloadNextFile();
}
void DownloadNextFile() {
if (downloadQueue.Count > 0) {
var item = downloadQueue.Dequeue();
currentDownloadItem = item;
// Download if the zip file is not existing
if (!File.Exists(item.Filename)) {
label.Text = item.URL;
webClient.DownloadFileAsync(new Uri(item.URL), item.Filename);
} else {
progressBar.Value = 100;
label.Text = "Use local " + item.Filename;
DownloadFileCompleted(null, null);
}
} else {
npmSetup();
}
}
void npmSetup() {
labelData.Text = "";
var npm = "..\\node\\npm.cmd";
var cmd = $"{npm} ci --production & {npm} run download-dist & exit";
var startInfo = new ProcessStartInfo {
FileName = "cmd.exe",
Arguments = $"/k \"{cmd}\"",
RedirectStandardOutput = false,
RedirectStandardError = false,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = false,
WorkingDirectory = "core"
};
var process = new Process();
process.StartInfo = startInfo;
process.EnableRaisingEvents = true;
process.Exited += (_, e) => {
progressBar.Value = 100;
if (process.ExitCode == 0) {
Task.Delay(2000).ContinueWith(_ => {
Application.Restart();
});
label.Text = "Done";
} else {
label.Text = "Failed, exit code: " + process.ExitCode;
}
};
process.Start();
label.Text = "Installing dependencies and download dist files";
progressBar.Value = 50;
process.WaitForExit();
}
void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) {
progressBar.Value = e.ProgressPercentage;
var total = e.TotalBytesToReceive / 1024;
var current = e.BytesReceived / 1024;
if (total > 0) {
labelData.Text = $"{current}KB/{total}KB";
}
}
void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) {
Extract(currentDownloadItem);
DownloadNextFile();
}
void Extract(DownloadItem item) {
if (Directory.Exists(item.TargetFolder)) {
var dir = new DirectoryInfo(item.TargetFolder);
dir.Delete(true);
}
if (Directory.Exists("temp")) {
var dir = new DirectoryInfo("temp");
dir.Delete(true);
}
labelData.Text = $"Extracting {item.Filename}...";
ZipFile.ExtractToDirectory(item.Filename, "temp");
string[] dirList;
// Move to the correct level
dirList = Directory.GetDirectories("temp");
if (dirList.Length > 0) {
var dir = dirList[0];
// As sometime ExtractToDirectory is still locking the directory, loop until ok
while (true) {
try {
Directory.Move(dir, item.TargetFolder);
break;
} catch (Exception exception) {
Thread.Sleep(1000);
}
}
} else {
MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found.");
}
labelData.Text = $"Extracted";
if (Directory.Exists("temp")) {
var dir = new DirectoryInfo("temp");
dir.Delete(true);
}
File.Delete(item.Filename);
}
}
public class DownloadItem {
public string URL { get; set; }
public string Filename { get; set; }
public string TargetFolder { get; set; }
}
}

View File

@@ -1,377 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA
AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//
/wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r
u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw
8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq
s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t
163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA
AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp
sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr
u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw
8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo
rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr
uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv
5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq
tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp
sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s
vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp
r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr
uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv//
/wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po
rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr
t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho
qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i
dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n
qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh
b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls
w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg
av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr
uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf
ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq
t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0
9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn
of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi
dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh
bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg
af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x
74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De
Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq
tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd
Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq
s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA
AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp
sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf
Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e
Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl
mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds
wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx
8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp
sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po
rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu
2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho
q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo
6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy
8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A
AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD
AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA
AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH
AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA
AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf//
AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA
gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0
9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj
4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA
AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv
3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy
8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq
s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp
sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx
8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo
rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s
vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp
sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr
uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx
8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq
tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx
8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi
c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz
80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh
bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx
8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf
Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx
8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e
YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv
45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n
qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq
t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n
qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq
s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx
8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp
sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx
8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk
i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0
9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm
mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA
AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1
9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx
8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo
ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx
8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx
8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA
AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA
AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw
8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs
xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp
sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp
rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho
qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n
qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw
6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx
8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV
1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA
AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy
8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx
8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz
80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs
7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB
AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA=
</value>
</data>
</root>

View File

@@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
</Weavers>

View File

@@ -1,141 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -1,262 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
using Newtonsoft.Json;
using UptimeKuma.Properties;
namespace UptimeKuma {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args) {
var cwd = Path.GetDirectoryName(Application.ExecutablePath);
if (cwd != null) {
Environment.CurrentDirectory = cwd;
}
bool isIntranet = args.Contains("--intranet");
if (isIntranet) {
Console.WriteLine("The --intranet argument was provided, so we will not try to access the internet. The first time this application runs you'll need to run it without the --intranet param or copy the result from another machine to the intranet server.");
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new UptimeKumaApplicationContext(isIntranet));
}
}
public class UptimeKumaApplicationContext : ApplicationContext
{
private static Mutex mutex = null;
const string appName = "Uptime Kuma";
private NotifyIcon trayIcon;
private Process process;
private MenuItem statusMenuItem;
private MenuItem runWhenStarts;
private MenuItem openMenuItem;
private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
private readonly bool intranetOnly;
public UptimeKumaApplicationContext(bool intranetOnly) {
// Single instance only
bool createdNew;
mutex = new Mutex(true, appName, out createdNew);
if (!createdNew) {
return;
}
this.intranetOnly = intranetOnly;
var startingText = "Starting server...";
trayIcon = new NotifyIcon();
trayIcon.Text = startingText;
runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts);
runWhenStarts.Checked = registryKey.GetValue(appName) != null;
statusMenuItem = new MenuItem(startingText);
statusMenuItem.Enabled = false;
openMenuItem = new MenuItem("Open", Open);
openMenuItem.Enabled = false;
trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location);
trayIcon.ContextMenu = new ContextMenu(new MenuItem[] {
statusMenuItem,
openMenuItem,
//new("Debug Console", DebugConsole),
runWhenStarts,
new("Check for Update...", CheckForUpdate),
new("Visit GitHub...", VisitGitHub),
new("About", About),
new("Exit", Exit),
});
trayIcon.MouseDoubleClick += new MouseEventHandler(Open);
trayIcon.Visible = true;
var hasUpdateFile = File.Exists("update");
if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) {
// Go go go
StartProcess();
} else {
DownloadFiles();
}
}
void DownloadFiles() {
if (intranetOnly) {
return;
}
var form = new DownloadForm();
form.Closed += Exit;
form.Show();
}
private void RunWhenStarts(object sender, EventArgs e) {
if (registryKey == null) {
MessageBox.Show("Error: Unable to set startup registry key.");
return;
}
if (runWhenStarts.Checked) {
registryKey.DeleteValue(appName, false);
runWhenStarts.Checked = false;
} else {
registryKey.SetValue(appName, Application.ExecutablePath);
runWhenStarts.Checked = true;
}
}
void StartProcess() {
var startInfo = new ProcessStartInfo {
FileName = "node/node.exe",
Arguments = "server/server.js --data-dir=\"../data/\"",
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = "core"
};
process = new Process();
process.StartInfo = startInfo;
process.EnableRaisingEvents = true;
process.Exited += ProcessExited;
try {
process.Start();
//Open(null, null);
// Async task to check if the server is ready
Task.Run(() => {
var runningText = "Server is running";
using TcpClient tcpClient = new TcpClient();
while (true) {
try {
tcpClient.Connect("127.0.0.1", 3001);
statusMenuItem.Text = runningText;
openMenuItem.Enabled = true;
trayIcon.Text = runningText;
break;
} catch (Exception) {
System.Threading.Thread.Sleep(2000);
}
}
});
} catch (Exception e) {
MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error");
}
}
void StopProcess() {
process?.Kill();
}
void Open(object sender, EventArgs e) {
Process.Start("http://localhost:3001");
}
void DebugConsole(object sender, EventArgs e) {
}
void CheckForUpdate(object sender, EventArgs e) {
if (intranetOnly) {
return;
}
// Check version.json exists
if (File.Exists("version.json")) {
// Load version.json and compare with the latest version from GitHub
var currentVersionObj = JsonConvert.DeserializeObject<Version>(File.ReadAllText("version.json"));
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
var latestVersionObj = JsonConvert.DeserializeObject<Version>(versionJson);
// Compare version, if the latest version is newer, then update
if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) {
var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes) {
// Create a empty file `update`, so the app will download the core files again at startup
File.Create("update").Close();
trayIcon.Visible = false;
process?.Kill();
// Restart the app, it will download the core files again at startup
Application.Restart();
}
} else {
MessageBox.Show("You are using the latest version.");
}
}
}
void VisitGitHub(object sender, EventArgs e) {
if (intranetOnly) {
MessageBox.Show("You have parsed in --intranet so we will not try to access the internet or visit github.com, please go to https://github.com/louislam/uptime-kuma if you want to visit github.");
return;
}
Process.Start("https://github.com/louislam/uptime-kuma");
}
void About(object sender, EventArgs e)
{
MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info");
}
void Exit(object sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
trayIcon.Visible = false;
process?.Kill();
Application.Exit();
}
void ProcessExited(object sender, EventArgs e) {
if (process.ExitCode != 0) {
var line = "";
while (!process.StandardOutput.EndOfStream)
{
line += process.StandardOutput.ReadLine();
}
MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line);
}
trayIcon.Visible = false;
Application.Exit();
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Uptime Kuma")]
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Uptime Kuma")]
[assembly: AssemblyProduct("Uptime Kuma")]
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: AssemblyFileVersion("1.0.2.0")]

View File

@@ -1,62 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace UptimeKuma.Properties {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
"4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
"CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if ((resourceMan == null)) {
global::System.Resources.ResourceManager temp =
new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources",
typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get { return resourceCulture; }
set { resourceCulture = value; }
}
}
}

View File

@@ -1,117 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,23 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace UptimeKuma.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance =
((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get { return defaultInstance; }
}
}
}

View File

@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -1,203 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>UptimeKuma</RootNamespace>
<AssemblyName>uptime-kuma</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<ApplicationIcon>..\..\public\favicon.ico</ApplicationIcon>
<LangVersion>9</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\"</PostBuildEvent>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Win32.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll</HintPath>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.AppContext, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll</HintPath>
</Reference>
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Console, Version=4.0.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Console.4.3.1\lib\net46\System.Console.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Diagnostics.DiagnosticSource, Version=7.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Tracing, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Calendars, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll</HintPath>
</Reference>
<Reference Include="System.IO, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.4.3.0\lib\net462\System.IO.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Linq, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Linq.4.3.0\lib\net463\System.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Expressions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll</HintPath>
</Reference>
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.1.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Net.Sockets, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Reflection, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll</HintPath>
</Reference>
<Reference Include="System.Runtime, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Extensions, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Algorithms, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Encoding, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Primitives, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.X509Certificates, Version=4.1.1.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll</HintPath>
</Reference>
<Reference Include="System.Text.RegularExpressions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.ReaderWriter, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="DownloadForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="DownloadForm.Designer.cs">
<DependentUpon>DownloadForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Version.cs" />
<EmbeddedResource Include="DownloadForm.resx">
<DependentUpon>DownloadForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="..\..\public\favicon.ico">
<Link>favicon.ico</Link>
</None>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Content Include=".gitignore" />
<Content Include="app.manifest" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets'))" />
</Target>
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
</Project>

View File

@@ -1,16 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,3 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=UptimeKuma_002FProperties_002FResources/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,9 +0,0 @@
namespace UptimeKuma {
public class Version {
public string latest { get; set; }
public string slow { get; set; }
public string beta { get; set; }
public string nodejs { get; set; }
public string exe { get; set; }
}
}

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC Manifest Options
If you want to change the Windows User Account Control level replace the
requestedExecutionLevel node with one of the following.
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Specifying requestedExecutionLevel element will disable file and registry virtualization.
Remove this element if your application requires this virtualization for backwards
compatibility.
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.NETCore.Platforms" version="7.0.0" targetFramework="net472" />
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
<package id="Newtonsoft.Json" version="13.0.2" targetFramework="net472" />
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
<package id="System.Console" version="4.3.1" targetFramework="net472" />
<package id="System.Diagnostics.DiagnosticSource" version="7.0.1" targetFramework="net472" />
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
<package id="System.IO" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
</packages>

View File

@@ -15,7 +15,6 @@ if (newVersion) {
// Process package.json
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Process README.md

View File

@@ -1,6 +0,0 @@
console.log("Git Push and Publish the release note on github, then press any key to continue");
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on("data", process.exit.bind(process, 0));

64
extra/release/beta.mjs Normal file
View File

@@ -0,0 +1,64 @@
import "dotenv/config";
import {
ver,
buildDist,
buildImage,
checkDocker,
checkTagExists,
checkVersionFormat,
getRepoNames,
pressAnyKey,
execSync, uploadArtifacts,
} from "./lib.mjs";
import semver from "semver";
const repoNames = getRepoNames();
const version = process.env.RELEASE_BETA_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
console.log("RELEASE_BETA_VERSION:", version);
if (!githubToken) {
console.error("GITHUB_TOKEN is required");
process.exit(1);
}
// Check if the version is a valid semver
checkVersionFormat(version);
// Check if the semver identifier is "beta"
const semverIdentifier = semver.prerelease(version);
console.log("Semver identifier:", semverIdentifier);
if (semverIdentifier[0] !== "beta") {
console.error("VERSION should have a semver identifier of 'beta'");
process.exit(1);
}
// Check if docker is running
checkDocker();
// Check if the tag exists
await checkTagExists(repoNames, version);
// node extra/beta/update-version.js
execSync("node ./extra/beta/update-version.js");
// Build frontend dist
buildDist();
// Build slim image (rootless)
buildImage(repoNames, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image (rootless)
buildImage(repoNames, [ "beta-rootless", ver(version, "rootless") ], "rootless");
// Build slim image
buildImage(repoNames, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image
buildImage(repoNames, [ "beta", version ], "release");
await pressAnyKey();
// npm run upload-artifacts
uploadArtifacts(version, githubToken);

57
extra/release/final.mjs Normal file
View File

@@ -0,0 +1,57 @@
import "dotenv/config";
import {
ver,
buildDist,
buildImage,
checkDocker,
checkTagExists,
checkVersionFormat,
getRepoNames,
pressAnyKey, execSync, uploadArtifacts
} from "./lib.mjs";
const repoNames = getRepoNames();
const version = process.env.RELEASE_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
console.log("RELEASE_VERSION:", version);
if (!githubToken) {
console.error("GITHUB_TOKEN is required");
process.exit(1);
}
// Check if the version is a valid semver
checkVersionFormat(version);
// Check if docker is running
checkDocker();
// Check if the tag exists
await checkTagExists(repoNames, version);
// node extra/beta/update-version.js
execSync("node extra/update-version.js");
// Build frontend dist
buildDist();
// Build slim image (rootless)
buildImage(repoNames, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image (rootless)
buildImage(repoNames, [ "2-rootless", ver(version, "rootless") ], "rootless");
// Build slim image
buildImage(repoNames, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image
buildImage(repoNames, [ "next", "2", version ], "release");
await pressAnyKey();
// npm run upload-artifacts
uploadArtifacts(version, githubToken);
// node extra/update-wiki-version.js
execSync("node extra/update-wiki-version.js");

251
extra/release/lib.mjs Normal file
View File

@@ -0,0 +1,251 @@
import "dotenv/config";
import * as childProcess from "child_process";
import semver from "semver";
export const dryRun = process.env.RELEASE_DRY_RUN === "1";
if (dryRun) {
console.info("Dry run enabled.");
}
/**
* Check if docker is running
* @returns {void}
*/
export function checkDocker() {
try {
childProcess.execSync("docker ps");
} catch (error) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
}
/**
* Get Docker Hub repository name
*/
export function getRepoNames() {
if (process.env.RELEASE_REPO_NAMES) {
// Split by comma
return process.env.RELEASE_REPO_NAMES.split(",").map((name) => name.trim());
}
return [
"louislam/uptime-kuma",
"ghcr.io/louislam/uptime-kuma",
];
}
/**
* Build frontend dist
* @returns {void}
*/
export function buildDist() {
if (!dryRun) {
childProcess.execSync("npm run build", { stdio: "inherit" });
} else {
console.info("[DRY RUN] npm run build");
}
}
/**
* Build docker image and push to Docker Hub
* @param {string[]} repoNames Docker Hub repository names
* @param {string[]} tags Docker image tags
* @param {string} target Dockerfile's target name
* @param {string} buildArgs Docker build args
* @param {string} dockerfile Path to Dockerfile
* @param {string} platform Build platform
* @returns {void}
*/
export function buildImage(repoNames, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") {
let args = [
"buildx",
"build",
"-f",
dockerfile,
"--platform",
platform,
];
for (let repoName of repoNames) {
// Add tags
for (let tag of tags) {
args.push("-t", `${repoName}:${tag}`);
}
}
args = [
...args,
"--target",
target,
];
// Add build args
if (buildArgs) {
args.push("--build-arg", buildArgs);
}
args = [
...args,
".",
"--push",
];
if (!dryRun) {
childProcess.spawnSync("docker", args, { stdio: "inherit" });
} else {
console.log(`[DRY RUN] docker ${args.join(" ")}`);
}
}
/**
* Check if the version already exists on Docker Hub
* TODO: use semver to compare versions if it is greater than the previous?
* @param {string[]} repoNames repository name (Only check the name with single slash)
* @param {string} version Version to check
* @returns {void}
*/
export async function checkTagExists(repoNames, version) {
// Skip if the tag is not on Docker Hub
// louislam/uptime-kuma
let dockerHubRepoNames = repoNames.filter((name) => {
return name.split("/").length === 2;
});
for (let repoName of dockerHubRepoNames) {
await checkTagExistsSingle(repoName, version);
}
}
/**
* Check if the version already exists on Docker Hub
* @param {string} repoName repository name
* @param {string} version Version to check
* @returns {Promise<void>}
*/
export async function checkTagExistsSingle(repoName, version) {
console.log(`Checking if version ${version} exists on Docker Hub:`, repoName);
// Get a list of tags from the Docker Hub repository
let tags = [];
// It is mainly to check my careless mistake that I forgot to update the release version in .env, so `page_size` is set to 100 is enough, I think.
const response = await fetch(`https://hub.docker.com/v2/repositories/${repoName}/tags/?page_size=100`);
if (response.ok) {
const data = await response.json();
tags = data.results.map((tag) => tag.name);
} else {
console.error("Failed to get tags from Docker Hub");
process.exit(1);
}
// Check if the version already exists
if (tags.includes(version)) {
console.error(`Version ${version} already exists`);
process.exit(1);
}
}
/**
* Check the version format
* @param {string} version Version to check
* @returns {void}
*/
export function checkVersionFormat(version) {
if (!version) {
console.error("VERSION is required");
process.exit(1);
}
// Check the version format, it should be a semver and must be like this: "2.0.0-beta.0"
if (!semver.valid(version)) {
console.error("VERSION is not a valid semver version");
process.exit(1);
}
}
/**
* Press any key to continue
* @returns {Promise<void>}
*/
export function pressAnyKey() {
console.log("Git Push and Publish the release note on github, then press any key to continue");
process.stdin.setRawMode(true);
process.stdin.resume();
return new Promise(resolve => process.stdin.once("data", data => {
process.stdin.setRawMode(false);
process.stdin.pause();
resolve();
}));
}
/**
* Append version identifier
* @param {string} version Version
* @param {string} identifier Identifier
* @returns {string} Version with identifier
*/
export function ver(version, identifier) {
const obj = semver.parse(version);
if (obj.prerelease.length === 0) {
obj.prerelease = [ identifier ];
} else {
obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-");
}
return obj.format();
}
/**
* Upload artifacts to GitHub
* docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain
* @param {string} version Version
* @param {string} githubToken GitHub token
* @returns {void}
*/
export function uploadArtifacts(version, githubToken) {
let args = [
"buildx",
"build",
"-f",
"docker/dockerfile",
"--platform",
"linux/amd64",
"-t",
"louislam/uptime-kuma:upload-artifact",
"--build-arg",
`VERSION=${version}`,
"--build-arg",
"GITHUB_TOKEN",
"--target",
"upload-artifact",
".",
"--progress",
"plain",
];
if (!dryRun) {
childProcess.spawnSync("docker", args, {
stdio: "inherit",
env: {
...process.env,
GITHUB_TOKEN: githubToken,
},
});
} else {
console.log(`[DRY RUN] docker ${args.join(" ")}`);
}
}
/**
* Execute a command
* @param {string} cmd Command to execute
* @returns {void}
*/
export function execSync(cmd) {
if (!dryRun) {
childProcess.execSync(cmd, { stdio: "inherit" });
} else {
console.info(`[DRY RUN] ${cmd}`);
}
}

16
extra/release/nightly.mjs Normal file
View File

@@ -0,0 +1,16 @@
import { buildDist, buildImage, checkDocker, getRepoNames } from "./lib.mjs";
// Docker Hub repository name
const repoNames = getRepoNames();
// Check if docker is running
checkDocker();
// Build frontend dist (it will build on the host machine, TODO: build on a container?)
buildDist();
// Build full image (rootless)
buildImage(repoNames, [ "nightly2-rootless" ], "nightly-rootless");
// Build full image
buildImage(repoNames, [ "nightly2" ], "nightly");

View File

@@ -0,0 +1,6 @@
import { uploadArtifacts } from "./lib.mjs";
const version = process.env.RELEASE_BETA_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
uploadArtifacts(version, githubToken);

View File

@@ -0,0 +1,6 @@
import { uploadArtifacts } from "./lib.mjs";
const version = process.env.RELEASE_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
uploadArtifacts(version, githubToken);

View File

@@ -0,0 +1,25 @@
// For #5231
const fs = require("fs");
let path = "../src/lang";
// list directories in the lang directory
let jsonFileList = fs.readdirSync(path);
for (let jsonFile of jsonFileList) {
if (!jsonFile.endsWith(".json")) {
continue;
}
let jsonPath = path + "/" + jsonFile;
let langData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
for (let key in langData) {
if (langData[key] === "") {
delete langData[key];
}
}
fs.writeFileSync(jsonPath, JSON.stringify(langData, null, 4) + "\n");
}

View File

@@ -0,0 +1,24 @@
const { R } = require("redbean-node");
const Database = require("../server/database");
const args = require("args-parser")(process.argv);
const { Settings } = require("../server/settings");
const main = async () => {
console.log("Connecting the database");
Database.initDataDir(args);
await Database.connect(false, false, true);
console.log("Deleting all data from aggregate tables");
await R.exec("DELETE FROM stat_minutely");
await R.exec("DELETE FROM stat_hourly");
await R.exec("DELETE FROM stat_daily");
console.log("Resetting the aggregate table state");
await Settings.set("migrateAggregateTableState", "");
await Database.close();
console.log("Done");
};
main();

View File

@@ -1,9 +0,0 @@
// Check if docker is running
const { exec } = require("child_process");
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

View File

@@ -5,7 +5,7 @@ const util = require("../src/util");
util.polyfill();
const newVersion = process.env.VERSION;
const newVersion = process.env.RELEASE_VERSION;
console.log("New Version: " + newVersion);

View File

@@ -1,7 +1,7 @@
const childProcess = require("child_process");
const fs = require("fs");
const newVersion = process.env.VERSION;
const newVersion = process.env.RELEASE_VERSION;
if (!newVersion) {
console.log("Missing version");

7996
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "2.0.0-dev",
"version": "2.0.0-beta.2",
"license": "MIT",
"repository": {
"type": "git",
@@ -34,20 +34,14 @@
"playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push",
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push",
"build-docker-slim-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim-rootless -t louislam/uptime-kuma:$VERSION-slim-rootless --target rootless --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-full-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-rootless -t louislam/uptime-kuma:$VERSION-rootless --target rootless . --push",
"build-docker-nightly-rootless": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2-rootless --target nightly-rootless . --push",
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist",
"upload-artifacts": "node extra/release/upload-artifacts.mjs",
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
"setup": "git checkout 1.23.16 && npm ci --omit dev && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -58,8 +52,9 @@
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"release-final": "node ./extra/release/final.mjs",
"release-beta": "node ./extra/release/beta.mjs",
"release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
@@ -68,7 +63,8 @@
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@@ -76,7 +72,7 @@
"@louislam/sqlite3": "15.1.6",
"@vvo/tzdb": "^6.125.0",
"args-parser": "~1.3.0",
"axios": "~0.28.1",
"axios": "~0.29.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"chardet": "~1.4.0",
@@ -99,8 +95,8 @@
"html-escaper": "^3.0.3",
"http-cookie-agent": "~5.0.4",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"http-proxy-agent": "~7.0.2",
"https-proxy-agent": "~7.0.6",
"iconv-lite": "~0.6.3",
"isomorphic-ws": "^5.0.0",
"jsesc": "~3.0.2",
@@ -108,7 +104,7 @@
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
"kafkajs": "^2.2.4",
"knex": "^2.4.2",
"knex": "~3.1.0",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"marked": "^14.0.0",
@@ -116,13 +112,13 @@
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"mysql2": "~3.11.3",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
"nostr-tools": "^1.13.1",
"nostr-tools": "^2.10.4",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
@@ -139,7 +135,7 @@
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socks-proxy-agent": "6.1.1",
"socks-proxy-agent": "~8.0.5",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
@@ -147,13 +143,15 @@
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.1.1",
"@actions/github": "~6.0.0",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
@@ -171,7 +169,7 @@
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~3.1.7",
"dompurify": "~3.2.4",
"eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
@@ -181,7 +179,7 @@
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4",
"prismjs": "~1.29.0",
"prismjs": "~1.30.0",
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
@@ -189,16 +187,17 @@
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",
"vite": "~5.4.15",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.0.15",
"vue": "~3.4.2",
"vue-chartjs": "~5.2.0",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.2.2",
"vue-i18n": "~9.14.3",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "~2.0.0-alpha.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -6,6 +6,11 @@ const knex = require("knex");
const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise");
const { Settings } = require("./settings");
const { UptimeCalculator } = require("./uptime-calculator");
const dayjs = require("dayjs");
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
/**
* Database & App Data Folder
@@ -194,6 +199,14 @@ class Database {
* @returns {Promise<void>}
*/
static async connect(testMode = false, autoloadModels = true, noLog = false) {
// Patch "mysql2" knex client
// Workaround: Tried extending the ColumnCompiler class, but it didn't work for unknown reasons, so I override the function via prototype
const { getDialectByNameOrAlias } = require("knex/lib/dialects");
const mysql2 = getDialectByNameOrAlias("mysql2");
mysql2.prototype.columnCompiler = function () {
return new KumaColumnCompiler(this, ...arguments);
};
const acquireConnectionTimeout = 120 * 1000;
let dbConfig;
try {
@@ -283,7 +296,7 @@ class Database {
client: "mysql2",
connection: {
socketPath: embeddedMariaDB.socketPath,
user: "node",
user: embeddedMariaDB.username,
database: "kuma",
timezone: "Z",
typeCast: function (field, next) {
@@ -379,9 +392,11 @@ class Database {
/**
* Patch the database
* @param {number} port Start the migration server for aggregate tables on this port if provided
* @param {string} hostname Start the migration server for aggregate tables on this hostname if provided
* @returns {Promise<void>}
*/
static async patch() {
static async patch(port = undefined, hostname = undefined) {
// Still need to keep this for old versions of Uptime Kuma
if (Database.dbConfig.type === "sqlite") {
await this.patchSqlite();
@@ -391,9 +406,23 @@ class Database {
// https://knexjs.org/guide/migrations.html
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
try {
// Disable foreign key check for SQLite
// Known issue of knex: https://github.com/drizzle-team/drizzle-orm/issues/1813
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = OFF");
}
await R.knex.migrate.latest({
directory: Database.knexMigrationsPath,
});
// Enable foreign key check for SQLite
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = ON");
}
await this.migrateAggregateTable(port, hostname);
} catch (e) {
// Allow missing patch files for downgrade or testing pr.
if (e.message.includes("the following files are missing:")) {
@@ -711,6 +740,175 @@ class Database {
}
}
/**
* Migrate the old data in the heartbeat table to the new format (stat_daily, stat_hourly, stat_minutely)
* It should be run once while upgrading V1 to V2
*
* Normally, it should be in transaction, but UptimeCalculator wasn't designed to be in transaction before that.
* I don't want to heavily modify the UptimeCalculator, so it is not in transaction.
* Run `npm run reset-migrate-aggregate-table-state` to reset, in case the migration is interrupted.
* @param {number} port Start the migration server on this port if provided
* @param {string} hostname Start the migration server on this hostname if provided
* @returns {Promise<void>}
*/
static async migrateAggregateTable(port, hostname = undefined) {
log.debug("db", "Enter Migrate Aggregate Table function");
// Add a setting for 2.0.0-dev users to skip this migration
if (process.env.SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE === "1") {
log.warn("db", "SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE is set to 1, skipping aggregate table migration forever (for 2.0.0-dev users)");
await Settings.set("migrateAggregateTableState", "migrated");
}
let migrateState = await Settings.get("migrateAggregateTableState");
// Skip if already migrated
// If it is migrating, it possibly means the migration was interrupted, or the migration is in progress
if (migrateState === "migrated") {
log.debug("db", "Migrated aggregate table already, skip");
return;
} else if (migrateState === "migrating") {
log.warn("db", "Aggregate table migration is already in progress, or it was interrupted");
throw new Error("Aggregate table migration is already in progress");
}
/**
* Start migration server for displaying the migration status
* @type {SimpleMigrationServer}
*/
let migrationServer;
let msg;
if (port) {
migrationServer = new SimpleMigrationServer();
await migrationServer.start(port, hostname);
}
log.info("db", "Migrating Aggregate Table");
log.info("db", "Getting list of unique monitors");
// Get a list of unique monitors from the heartbeat table, using raw sql
let monitors = await R.getAll(`
SELECT DISTINCT monitor_id
FROM heartbeat
ORDER BY monitor_id ASC
`);
// Stop if stat_* tables are not empty
for (let table of [ "stat_minutely", "stat_hourly", "stat_daily" ]) {
let countResult = await R.getRow(`SELECT COUNT(*) AS count FROM ${table}`);
let count = countResult.count;
if (count > 0) {
log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`);
await migrationServer?.stop();
return;
}
}
await Settings.set("migrateAggregateTableState", "migrating");
let progressPercent = 0;
let part = 100 / monitors.length;
let i = 1;
for (let monitor of monitors) {
// Get a list of unique dates from the heartbeat table, using raw sql
let dates = await R.getAll(`
SELECT DISTINCT DATE(time) AS date
FROM heartbeat
WHERE monitor_id = ?
ORDER BY date ASC
`, [
monitor.monitor_id
]);
for (let date of dates) {
// New Uptime Calculator
let calculator = new UptimeCalculator();
calculator.monitorID = monitor.monitor_id;
calculator.setMigrationMode(true);
// Get all the heartbeats for this monitor and date
let heartbeats = await R.getAll(`
SELECT status, ping, time
FROM heartbeat
WHERE monitor_id = ?
AND DATE(time) = ?
ORDER BY time ASC
`, [ monitor.monitor_id, date.date ]);
if (heartbeats.length > 0) {
msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`;
log.info("db", msg);
migrationServer?.update(msg);
}
for (let heartbeat of heartbeats) {
await calculator.update(heartbeat.status, parseFloat(heartbeat.ping), dayjs(heartbeat.time));
}
progressPercent += (Math.round(part / dates.length * 100) / 100);
// Lazy to fix the floating point issue, it is acceptable since it is just a progress bar
if (progressPercent > 100) {
progressPercent = 100;
}
}
i++;
}
msg = "Clearing non-important heartbeats";
log.info("db", msg);
migrationServer?.update(msg);
await Database.clearHeartbeatData(true);
await Settings.set("migrateAggregateTableState", "migrated");
await migrationServer?.stop();
if (monitors.length > 0) {
log.info("db", "Aggregate Table Migration Completed");
} else {
log.info("db", "No data to migrate");
}
}
/**
* Remove all non-important heartbeats from heartbeat table, keep last 24-hour or {KEEP_LAST_ROWS} rows for each monitor
* @param {boolean} detailedLog Log detailed information
* @returns {Promise<void>}
*/
static async clearHeartbeatData(detailedLog = false) {
let monitors = await R.getAll("SELECT id FROM monitor");
const sqlHourOffset = Database.sqlHourOffset();
for (let monitor of monitors) {
if (detailedLog) {
log.info("db", "Deleting non-important heartbeats for monitor " + monitor.id);
}
await R.exec(`
DELETE FROM heartbeat
WHERE monitor_id = ?
AND important = 0
AND time < ${sqlHourOffset}
AND id NOT IN (
SELECT id FROM ( -- written this way for Maria's support
SELECT id
FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT ?
) AS limited_ids
)
`, [
monitor.id,
-24,
monitor.id,
100,
]);
}
}
}
module.exports = Database;

View File

@@ -14,9 +14,15 @@ class EmbeddedMariaDB {
mariadbDataDir = "/app/data/mariadb";
runDir = "/app/data/run/mariadb";
runDir = "/app/data/run";
socketPath = this.runDir + "/mysqld.sock";
socketPath = this.runDir + "/mariadb.sock";
/**
* The username to connect to the MariaDB
* @type {string}
*/
username = null;
/**
* @type {ChildProcessWithoutNullStreams}
@@ -46,16 +52,42 @@ class EmbeddedMariaDB {
/**
* Start the embedded MariaDB
* @throws {Error} If the current user is not "node" or "root"
* @returns {Promise<void>|void} A promise that resolves when the MariaDB is started or void if it is already started
*/
start() {
// Check if the current user is "node" or "root"
this.username = require("os").userInfo().username;
if (this.username !== "node" && this.username !== "root") {
throw new Error("Embedded Mariadb supports only 'node' or 'root' user, but the current user is: " + this.username);
}
this.initDB();
this.startChildProcess();
return new Promise((resolve) => {
let interval = setInterval(() => {
if (this.started) {
clearInterval(interval);
resolve();
} else {
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
}
}, 1000);
});
}
/**
* Start the child process
* @returns {void}
*/
startChildProcess() {
if (this.childProcess) {
log.info("mariadb", "Already started");
return;
}
this.initDB();
this.running = true;
log.info("mariadb", "Starting Embedded MariaDB");
this.childProcess = childProcess.spawn(this.exec, [
@@ -63,6 +95,8 @@ class EmbeddedMariaDB {
"--datadir=" + this.mariadbDataDir,
`--socket=${this.socketPath}`,
`--pid-file=${this.runDir}/mysqld.pid`,
// Don't add the following option, the mariadb will not report message to the console, which affects initDBAfterStarted()
// "--log-error=" + `${this.mariadbDataDir}/mariadb-error.log`,
]);
this.childProcess.on("close", (code) => {
@@ -72,8 +106,8 @@ class EmbeddedMariaDB {
log.info("mariadb", "Stopped Embedded MariaDB: " + code);
if (code !== 0) {
log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
this.start();
log.error("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
this.startChildProcess();
}
});
@@ -86,7 +120,7 @@ class EmbeddedMariaDB {
});
let handler = (data) => {
log.debug("mariadb", data.toString("utf-8"));
log.info("mariadb", data.toString("utf-8"));
if (data.toString("utf-8").includes("ready for connections")) {
this.initDBAfterStarted();
}
@@ -94,17 +128,6 @@ class EmbeddedMariaDB {
this.childProcess.stdout.on("data", handler);
this.childProcess.stderr.on("data", handler);
return new Promise((resolve) => {
let interval = setInterval(() => {
if (this.started) {
clearInterval(interval);
resolve();
} else {
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
}
}, 1000);
});
}
/**
@@ -129,9 +152,11 @@ class EmbeddedMariaDB {
recursive: true,
});
let result = childProcess.spawnSync("mysql_install_db", [
let result = childProcess.spawnSync("mariadb-install-db", [
"--user=node",
"--ldata=" + this.mariadbDataDir,
"--auth-root-socket-user=node",
"--datadir=" + this.mariadbDataDir,
"--auth-root-authentication-method=socket",
]);
if (result.status !== 0) {
@@ -143,6 +168,17 @@ class EmbeddedMariaDB {
}
}
// Check the owner of the mariadb directory, and change it if necessary
let stat = fs.statSync(this.mariadbDataDir);
if (stat.uid !== 1000 || stat.gid !== 1000) {
fs.chownSync(this.mariadbDataDir, 1000, 1000);
}
// Check the permission of the mariadb directory, and change it if it is not 755
if (stat.mode !== 0o755) {
fs.chmodSync(this.mariadbDataDir, 0o755);
}
if (!fs.existsSync(this.runDir)) {
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
fs.mkdirSync(this.runDir, {
@@ -150,6 +186,13 @@ class EmbeddedMariaDB {
});
}
stat = fs.statSync(this.runDir);
if (stat.uid !== 1000 || stat.gid !== 1000) {
fs.chownSync(this.runDir, 1000, 1000);
}
if (stat.mode !== 0o755) {
fs.chmodSync(this.runDir, 0o755);
}
}
/**
@@ -159,7 +202,7 @@ class EmbeddedMariaDB {
async initDBAfterStarted() {
const connection = mysql.createConnection({
socketPath: this.socketPath,
user: "node",
user: this.username,
});
let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");

View File

@@ -1,21 +1,22 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server");
const Database = require("../database");
const { Settings } = require("../settings");
const dayjs = require("dayjs");
const DEFAULT_KEEP_PERIOD = 180;
const DEFAULT_KEEP_PERIOD = 365;
/**
* Clears old data from the heartbeat table of the database.
* Clears old data from the heartbeat table and the stat_daily of the database.
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
*/
const clearOldData = async () => {
let period = await setting("keepDataPeriodDays");
await Database.clearHeartbeatData();
let period = await Settings.get("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
@@ -25,23 +26,28 @@ const clearOldData = async () => {
parsedPeriod = parseInt(period);
} catch (_) {
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
if (parsedPeriod < 1) {
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
} else {
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
const sqlHourOffset = Database.sqlHourOffset();
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
[ parsedPeriod * -24 ]
);
// Heartbeat
await R.exec("DELETE FROM heartbeat WHERE time < " + sqlHourOffset, [
parsedPeriod * -24,
]);
let timestamp = dayjs().subtract(parsedPeriod, "day").utc().startOf("day").unix();
// stat_daily
await R.exec("DELETE FROM stat_daily WHERE timestamp < ? ", [
timestamp,
]);
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA optimize;");
@@ -50,6 +56,8 @@ const clearOldData = async () => {
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
}
}
log.debug("clearOldData", "Data cleared.");
};
module.exports = {

View File

@@ -153,6 +153,7 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
};
@@ -183,6 +184,8 @@ class Monitor extends BeanModel {
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
rabbitmqUsername: this.rabbitmqUsername,
rabbitmqPassword: this.rabbitmqPassword,
};
}
@@ -1431,7 +1434,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will expire in ${daysRemaining} days`);
sent = true;
} catch (e) {
log.error("monitor", "Cannot send cert notification to " + notification.name);
@@ -1508,10 +1511,8 @@ class Monitor extends BeanModel {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
@@ -1521,13 +1522,11 @@ class Monitor extends BeanModel {
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, tag.name, tag.color
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, monitor_tag.value, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
}
/**
@@ -1567,6 +1566,9 @@ class Monitor extends BeanModel {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
monitor_id: row.monitor_id,
value: row.value,
name: row.name,
color: row.color
});

View File

@@ -485,7 +485,7 @@ function ApiCache() {
}
if (typeof duration === "string") {
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
let split = duration.match(/^([\d\.,]+)\s?([a-zA-Z]+)$/);
if (split.length === 3) {
let len = parseFloat(split[1]);

View File

@@ -0,0 +1,67 @@
const { MonitorType } = require("./monitor-type");
const { log, UP, DOWN } = require("../../src/util");
const { axiosAbortSignal } = require("../util-server");
const axios = require("axios");
class RabbitMqMonitorType extends MonitorType {
name = "rabbitmq";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
let baseUrls = [];
try {
baseUrls = JSON.parse(monitor.rabbitmqNodes);
} catch (error) {
throw new Error("Invalid RabbitMQ Nodes");
}
heartbeat.status = DOWN;
for (let baseUrl of baseUrls) {
try {
// Without a trailing slash, path in baseUrl will be removed. https://example.com/api -> https://example.com
if ( !baseUrl.endsWith("/") ) {
baseUrl += "/";
}
const options = {
// Do not start with slash, it will strip the trailing slash from baseUrl
url: new URL("api/health/checks/alarms/", baseUrl).href,
method: "get",
timeout: monitor.timeout * 1000,
headers: {
"Accept": "application/json",
"Authorization": "Basic " + Buffer.from(`${monitor.rabbitmqUsername || ""}:${monitor.rabbitmqPassword || ""}`).toString("base64"),
},
signal: axiosAbortSignal((monitor.timeout + 10) * 1000),
// Capture reason for 503 status
validateStatus: (status) => status === 200 || status === 503,
};
log.debug("monitor", `[${monitor.name}] Axios Request: ${JSON.stringify(options)}`);
const res = await axios.request(options);
log.debug("monitor", `[${monitor.name}] Axios Response: status=${res.status} body=${JSON.stringify(res.data)}`);
if (res.status === 200) {
heartbeat.status = UP;
heartbeat.msg = "OK";
break;
} else if (res.status === 503) {
heartbeat.msg = res.data.reason;
} else {
heartbeat.msg = `${res.status} - ${res.statusText}`;
}
} catch (error) {
if (axios.isCancel(error)) {
heartbeat.msg = "Request timed out";
log.debug("monitor", `[${monitor.name}] Request timed out`);
} else {
log.debug("monitor", `[${monitor.name}] Axios Error: ${JSON.stringify(error.message)}`);
heartbeat.msg = error.message;
}
}
}
}
}
module.exports = {
RabbitMqMonitorType,
};

View File

@@ -240,6 +240,14 @@ class RealBrowserMonitorType extends MonitorType {
const context = await browser.newContext();
const page = await context.newPage();
// Prevent Local File Inclusion
// Accept only http:// and https://
// https://github.com/louislam/uptime-kuma/security/advisories/GHSA-2qgm-m29m-cj2h
let url = new URL(monitor.url);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Invalid url protocol, only http and https are allowed.");
}
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,

View File

@@ -0,0 +1,35 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Elks extends NotificationProvider {
name = "Elks";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.46elks.com/a1/sms";
try {
let data = new URLSearchParams();
data.append("from", notification.elksFromNumber);
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Elks;

View File

@@ -17,7 +17,7 @@ class AliyunSMS extends NotificationProvider {
if (heartbeatJSON != null) {
let msgBody = JSON.stringify({
name: monitorJSON["name"],
time: heartbeatJSON["time"],
time: heartbeatJSON["localDateTime"],
status: this.statusToString(heartbeatJSON["status"]),
msg: heartbeatJSON["msg"],
});

View File

@@ -87,7 +87,6 @@ class DingDing extends NotificationProvider {
* @returns {string} Status
*/
statusToString(status) {
// TODO: Move to notification-provider.js to avoid repetition in classes
switch (status) {
case DOWN:
return "DOWN";

View File

@@ -48,7 +48,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAdress(monitorJSON),
value: this.extractAddress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@@ -85,7 +85,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAdress(monitorJSON),
value: this.extractAddress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

View File

@@ -72,6 +72,7 @@ class GoogleChat extends NotificationProvider {
// construct json data
let data = {
fallbackText: chatHeader["title"],
cardsV2: [
{
card: {

View File

@@ -1,11 +1,10 @@
const NotificationProvider = require("./notification-provider");
const {
relayInit,
getPublicKey,
getEventHash,
getSignature,
finalizeEvent,
Relay,
kinds,
nip04,
nip19
nip19,
} = require("nostr-tools");
// polyfills for node versions
@@ -31,7 +30,6 @@ class Nostr extends NotificationProvider {
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
@@ -39,34 +37,41 @@ class Nostr extends NotificationProvider {
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
kind: kinds.EncryptedDirectMessage,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = getSignature(event, senderPrivateKey);
events.push(event);
const signedEvent = finalizeEvent(event, senderPrivateKey);
events.push(signedEvent);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
const relay = await Relay.connect(relayUrl);
let eventIndex = 0;
// Publish events
for (const event of events) {
relay.publish(event);
}
// Authenticate to the relay, if required
try {
await relay.publish(events[0]);
eventIndex = 1;
} catch (error) {
continue;
if (relay.challenge) {
await relay.auth(async (evt) => {
return finalizeEvent(evt, senderPrivateKey);
});
}
}
try {
for (let i = eventIndex; i < events.length; i++) {
await relay.publish(events[i]);
}
successfulRelays++;
} catch (error) {
console.error(`Failed to publish event to ${relayUrl}:`, error);
} finally {
relay.close();
}
@@ -90,7 +95,7 @@ class Nostr extends NotificationProvider {
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
throw new Error(`Failed to decode private key for sender ${sender}: ${error.message}`);
}
}
@@ -109,10 +114,10 @@ class Nostr extends NotificationProvider {
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
throw new Error(`Recipient ${recipient} is not an npub`);
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
throw new Error(`Error decoding recipient ${recipient}: ${error}`);
}
}
return publicKeys;

View File

@@ -1,3 +1,6 @@
const { Liquid } = require("liquidjs");
const { DOWN } = require("../../src/util");
class NotificationProvider {
/**
@@ -24,7 +27,7 @@ class NotificationProvider {
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAdress(monitorJSON) {
extractAddress(monitorJSON) {
if (!monitorJSON) {
return "";
}
@@ -49,6 +52,50 @@ class NotificationProvider {
}
}
/**
* Renders a message template with notification context
* @param {string} template the template
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {Promise<string>} rendered template
*/
async renderTemplate(template, msg, monitorJSON, heartbeatJSON) {
const engine = new Liquid();
const parsedTpl = engine.parse(template);
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAddress(monitorJSON);
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
const context = {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
return engine.render(parsedTpl, context);
}
/**
* Throws an error
* @param {any} error The error to throw

View File

@@ -11,7 +11,8 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`;
// capture group below is nessesary to prevent an ReDOS-attack
const url = `${serverUrl.trim().replace(/([^/])\/+$/, "$1")}/message/push`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;

View File

@@ -0,0 +1,56 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class PushPlus extends NotificationProvider {
name = "PushPlus";
/**
* @inheritdoc
* @param {BeanModel} notification Notification object
* @param {string} msg Message content
* @param {?object} monitorJSON Monitor details
* @param {?object} heartbeatJSON Heartbeat details
* @returns {Promise<string>} Success message
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://www.pushplus.plus/send";
try {
const config = {
headers: {
"Content-Type": "application/json",
},
};
const params = {
"token": notification.pushPlusSendKey,
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"content": msg,
"template": "html"
};
await axios.post(url, params, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Get the formatted title for message
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
}
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
}
return title;
}
}
module.exports = PushPlus;

View File

@@ -0,0 +1,65 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SendGrid extends NotificationProvider {
name = "SendGrid";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};
let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};
// Add CC recipients if provided
if (notification.sendgridCcEmail) {
personalizations.cc = notification.sendgridCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
// Add BCC recipients if provided
if (notification.sendgridBccEmail) {
personalizations.bcc = notification.sendgridBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
let data = {
personalizations: [ personalizations ],
from: { email: notification.sendgridFromEmail.trim() },
subject:
notification.sendgridSubject ||
"Notification from Your Uptime Kuma",
content: [
{
type: "text/plain",
value: msg,
},
],
};
await axios.post(
"https://api.sendgrid.com/v3/mail/send",
data,
config
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SendGrid;

View File

@@ -12,8 +12,9 @@ class ServerChan extends NotificationProvider {
const okMsg = "Sent Successfully.";
// serverchan3 requires sending via ft07.com
const url = String(notification.serverChanSendKey).startsWith("sctp")
? `https://${notification.serverChanSendKey}.push.ft07.com/send`
const matchResult = String(notification.serverChanSendKey).match(/^sctp(\d+)t/i);
const url = matchResult && matchResult[1]
? `https://${matchResult[1]}.push.ft07.com/send/${notification.serverChanSendKey}.send`
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
try {

View File

@@ -32,7 +32,7 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = this.extractAdress(monitorJSON);
let address = this.extractAddress(monitorJSON);
if (address !== "") {
address = `(${address}) `;
}

View File

@@ -18,7 +18,7 @@ class SIGNL4 extends NotificationProvider {
msg,
// Source system
"X-S4-SourceSystem": "UptimeKuma",
monitorUrl: this.extractAdress(monitorJSON),
monitorUrl: this.extractAddress(monitorJSON),
};
const config = {

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { getMonitorRelativeURL, UP, log } = require("../../src/util");
class Slack extends NotificationProvider {
name = "slack";
@@ -32,7 +32,7 @@ class Slack extends NotificationProvider {
* @param {object} monitorJSON The monitor config
* @returns {Array} The relevant action objects
*/
static buildActions(baseURL, monitorJSON) {
buildActions(baseURL, monitorJSON) {
const actions = [];
if (baseURL) {
@@ -48,17 +48,22 @@ class Slack extends NotificationProvider {
}
const address = this.extractAdress(monitorJSON);
const address = this.extractAddress(monitorJSON);
if (address) {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit site",
},
"value": "Site",
"url": address,
});
try {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit site",
},
"value": "Site",
"url": new URL(address),
});
} catch (e) {
log.debug("slack", `Failed to parse address ${address} as URL`);
}
}
return actions;
@@ -73,7 +78,7 @@ class Slack extends NotificationProvider {
* @param {string} msg The message body
* @returns {Array<object>} The rich content blocks for the Slack message
*/
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
//create an array to dynamically add blocks
const blocks = [];
@@ -150,7 +155,7 @@ class Slack extends NotificationProvider {
data.attachments.push(
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
"blocks": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {

View File

@@ -1,7 +1,5 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
const { DOWN } = require("../../src/util");
const { Liquid } = require("liquidjs");
class SMTP extends NotificationProvider {
name = "smtp";
@@ -53,15 +51,11 @@ class SMTP extends NotificationProvider {
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
if (customSubject !== "") {
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
}
}
@@ -78,43 +72,6 @@ class SMTP extends NotificationProvider {
return okMsg;
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAdress(monitorJSON);
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}
module.exports = SMTP;

View File

@@ -34,7 +34,7 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

View File

@@ -225,7 +225,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: this.extractAdress(monitorJSON),
monitorUrl: this.extractAddress(monitorJSON),
dashboardUrl: dashboardUrl,
});

View File

@@ -10,11 +10,22 @@ class TechulusPush extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
let data = {
"title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma",
"body": msg,
"timeSensitive": notification.pushTimeSensitive ?? true,
};
if (notification.pushChannel) {
data.channel = notification.pushChannel;
}
if (notification.pushSound) {
data.sound = notification.pushSound;
}
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
});
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -9,7 +9,7 @@ class Telegram extends NotificationProvider {
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.telegram.org";
const url = notification.telegramServerUrl ?? "https://api.telegram.org";
try {
let params = {
@@ -22,6 +22,14 @@ class Telegram extends NotificationProvider {
params.message_thread_id = notification.telegramMessageThreadID;
}
if (notification.telegramUseTemplate) {
params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON);
if (notification.telegramTemplateParseMode !== "plain") {
params.parse_mode = notification.telegramTemplateParseMode;
}
}
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, {
params: params,
});

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class WAHA extends NotificationProvider {
name = "waha";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Api-Key": notification.wahaApiKey,
}
};
let data = {
"session": notification.wahaSession,
"chatId": notification.wahaChatId,
"text": msg,
};
let url = notification.wahaApiUrl.replace(/([^/])\/+$/, "$1") + "/api/sendText";
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = WAHA;

View File

@@ -1,7 +1,6 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
const { Liquid } = require("liquidjs");
class Webhook extends NotificationProvider {
name = "webhook";
@@ -28,17 +27,7 @@ class Webhook extends NotificationProvider {
config.headers = formData.getHeaders();
data = formData;
} else if (notification.webhookContentType === "custom") {
// Initialize LiquidJS and parse the custom Body Template
const engine = new Liquid();
const tpl = engine.parse(notification.webhookCustomBody);
// Insert templated values into Body
data = await engine.render(tpl,
{
msg,
heartbeatJSON,
monitorJSON
});
data = await this.renderTemplate(notification.webhookCustomBody, msg, monitorJSON, heartbeatJSON);
}
if (notification.webhookAdditionalHeaders) {

View File

@@ -24,7 +24,7 @@ class Whapi extends NotificationProvider {
"body": msg,
};
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text";
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/([^/])\/+$/, "$1") + "/messages/text";
await axios.post(url, data, config);

View File

@@ -0,0 +1,57 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
class YZJ extends NotificationProvider {
name = "YZJ";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON !== null) {
msg = `${this.statusToString(heartbeatJSON["status"])} ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
const config = {
headers: {
"Content-Type": "application/json",
},
};
const params = {
content: msg
};
// yzjtype=0 => general robot
const url = `${notification.yzjWebHookUrl}?yzjtype=0&yzjtoken=${notification.yzjToken}`;
const result = await axios.post(url, params, config);
if (!result.data?.success) {
throw new Error(result.data?.errmsg ?? "yzj's server did not respond with the expected result");
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Convert status constant to string
* @param {string} status The status constant
* @returns {string} status
*/
statusToString(status) {
switch (status) {
case DOWN:
return "❌";
case UP:
return "✅";
default:
return status;
}
}
}
module.exports = YZJ;

View File

@@ -85,7 +85,7 @@ class ZohoCliq extends NotificationProvider {
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: this.extractAdress(monitorJSON),
monitorUrl: this.extractAddress(monitorJSON),
status: heartbeatJSON.status
});

View File

@@ -11,6 +11,7 @@ const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
@@ -38,6 +39,7 @@ const PromoSMS = require("./notification-providers/promosms");
const Pushbullet = require("./notification-providers/pushbullet");
const PushDeer = require("./notification-providers/pushdeer");
const Pushover = require("./notification-providers/pushover");
const PushPlus = require("./notification-providers/pushplus");
const Pushy = require("./notification-providers/pushy");
const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
@@ -63,10 +65,13 @@ const ServerChan = require("./notification-providers/serverchan");
const ZohoCliq = require("./notification-providers/zoho-cliq");
const SevenIO = require("./notification-providers/sevenio");
const Whapi = require("./notification-providers/whapi");
const WAHA = require("./notification-providers/waha");
const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const YZJ = require("./notification-providers/yzj");
class Notification {
@@ -95,6 +100,7 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Elks(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),
@@ -123,6 +129,7 @@ class Notification {
new Pushbullet(),
new PushDeer(),
new Pushover(),
new PushPlus(),
new Pushy(),
new RocketChat(),
new ServerChan(),
@@ -148,9 +155,12 @@ class Notification {
new ZohoCliq(),
new SevenIO(),
new Whapi(),
new WAHA(),
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
new SendGrid(),
new YZJ()
];
for (let item of list) {
if (! item.name) {

View File

@@ -1,7 +1,7 @@
const { R } = require("redbean-node");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { HttpProxyAgent } = require("http-proxy-agent");
const { HttpsProxyAgent } = require("https-proxy-agent");
const { SocksProxyAgent } = require("socks-proxy-agent");
const { debug } = require("../src/util");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { CookieJar } = require("tough-cookie");
@@ -100,17 +100,17 @@ class Proxy {
let jar = new CookieJar();
const proxyOptions = {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
cookies: { jar },
};
const proxyUrl = new URL(`${proxy.protocol}://${proxy.host}:${proxy.port}`);
if (proxy.auth) {
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
proxyUrl.username = proxy.username;
proxyUrl.password = proxy.password;
}
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
debug(`Proxy URL: ${proxyUrl.toString()}`);
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
@@ -122,15 +122,15 @@ class Proxy {
// eslint-disable-next-line no-case-declarations
const HttpsCookieProxyAgent = createCookieAgent(HttpsProxyAgent);
httpAgent = new HttpCookieProxyAgent({
...httpAgentOptions || {},
httpAgent = new HttpCookieProxyAgent(proxyUrl.toString(), {
...(httpAgentOptions || {}),
...proxyOptions,
});
httpsAgent = new HttpsCookieProxyAgent(proxyUrl.toString(), {
...(httpsAgentOptions || {}),
...proxyOptions,
});
httpsAgent = new HttpsCookieProxyAgent({
...httpsAgentOptions || {},
...proxyOptions,
});
break;
case "socks":
case "socks5":
@@ -138,10 +138,9 @@ class Proxy {
case "socks4":
// eslint-disable-next-line no-case-declarations
const SocksCookieProxyAgent = createCookieAgent(SocksProxyAgent);
agent = new SocksCookieProxyAgent({
agent = new SocksCookieProxyAgent(proxyUrl.toString(), {
...httpAgentOptions,
...httpsAgentOptions,
...proxyOptions,
tls: {
rejectUnauthorized: httpsAgentOptions.rejectUnauthorized,
},

View File

@@ -15,11 +15,13 @@ const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
slug = slug.toLowerCase();
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
slug = slug.toLowerCase();
await StatusPage.handleStatusPageRSSResponse(response, slug);
});
@@ -37,6 +39,7 @@ router.get("/status-page", cache("5 minutes"), async (request, response) => {
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
slug = slug.toLowerCase();
try {
// Get Status Page
@@ -69,6 +72,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
let uptimeList = {};
let slug = request.params.slug;
slug = slug.toLowerCase();
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
@@ -111,6 +115,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
slug = slug.toLowerCase();
try {
// Get Status Page
@@ -145,7 +150,8 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
// overall status-page status badge
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
const slug = request.params.slug;
let slug = request.params.slug;
slug = slug.toLowerCase();
const statusPageID = await StatusPage.slugToID(slug);
const {
label,

View File

@@ -718,6 +718,8 @@ let needSetup = false;
monitor.conditions = JSON.stringify(monitor.conditions);
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
bean.import(monitor);
bean.user_id = socket.userID;
@@ -868,6 +870,9 @@ let needSetup = false;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;
bean.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
bean.rabbitmqUsername = monitor.rabbitmqUsername;
bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions);
bean.validate();
@@ -1599,18 +1604,20 @@ let needSetup = false;
await server.start();
server.httpServer.listen(port, hostname, () => {
server.httpServer.listen(port, hostname, async () => {
if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`);
} else {
log.info("server", `Listening on ${port}`);
}
startMonitors();
await startMonitors();
// Put this here. Start background jobs after the db and server is ready to prevent clear up during db migration.
await initBackgroundJobs();
checkVersion.startInterval();
});
await initBackgroundJobs();
// Start cloudflared at the end if configured
await cloudflaredAutoStart(cloudflaredToken);
@@ -1709,7 +1716,7 @@ async function initDatabase(testMode = false) {
log.info("server", "Connected to the database");
// Patch the database
await Database.patch();
await Database.patch(port, hostname);
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
@@ -1804,7 +1811,11 @@ async function startMonitors() {
}
for (let monitor of list) {
await monitor.start(io);
try {
await monitor.start(io);
} catch (e) {
log.error("monitor", e);
}
// Give some delays, so all monitors won't make request at the same moment when just start the server.
await sleep(getRandomInt(300, 1000));
}

View File

@@ -220,13 +220,17 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete groups that are not in the list
log.debug("socket", "Delete groups that are not in the list");
const slots = groupIDList.map(() => "?").join(",");
if (groupIDList.length === 0) {
await R.exec("DELETE FROM `group` WHERE status_page_id = ?", [ statusPage.id ]);
} else {
const slots = groupIDList.map(() => "?").join(",");
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
}
const server = UptimeKumaServer.getInstance();
@@ -288,6 +292,7 @@ module.exports.statusPageSocketHandler = (socket) => {
ok: true,
msg: "successAdded",
msgi18n: true,
slug: slug
});
} catch (error) {

View File

@@ -12,7 +12,6 @@ class UptimeCalculator {
* @private
* @type {{string:UptimeCalculator}}
*/
static list = {};
/**
@@ -55,6 +54,15 @@ class UptimeCalculator {
lastHourlyStatBean = null;
lastMinutelyStatBean = null;
/**
* For migration purposes.
* @type {boolean}
*/
migrationMode = false;
statMinutelyKeepHour = 24;
statHourlyKeepDay = 30;
/**
* Get the uptime calculator for a monitor
* Initializes and returns the monitor if it does not exist
@@ -189,16 +197,19 @@ class UptimeCalculator {
/**
* @param {number} status status
* @param {number} ping Ping
* @param {dayjs.Dayjs} date Date (Only for migration)
* @returns {dayjs.Dayjs} date
* @throws {Error} Invalid status
*/
async update(status, ping = 0) {
let date = this.getCurrentDate();
async update(status, ping = 0, date) {
if (!date) {
date = this.getCurrentDate();
}
let flatStatus = this.flatStatus(status);
if (flatStatus === DOWN && ping > 0) {
log.warn("uptime-calc", "The ping is not effective when the status is DOWN");
log.debug("uptime-calc", "The ping is not effective when the status is DOWN");
}
let divisionKey = this.getMinutelyKey(date);
@@ -297,47 +308,61 @@ class UptimeCalculator {
}
await R.store(dailyStatBean);
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
hourlyStatBean.up = hourlyData.up;
hourlyStatBean.down = hourlyData.down;
hourlyStatBean.ping = hourlyData.avgPing;
hourlyStatBean.pingMin = hourlyData.minPing;
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
let currentDate = this.getCurrentDate();
// For migration mode, we don't need to store old hourly and minutely data, but we need 30-day's hourly data
// Run anyway for non-migration mode
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statHourlyKeepDay, "day"))) {
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
hourlyStatBean.up = hourlyData.up;
hourlyStatBean.down = hourlyData.down;
hourlyStatBean.ping = hourlyData.avgPing;
hourlyStatBean.pingMin = hourlyData.minPing;
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(hourlyStatBean);
}
await R.store(hourlyStatBean);
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
minutelyStatBean.up = minutelyData.up;
minutelyStatBean.down = minutelyData.down;
minutelyStatBean.ping = minutelyData.avgPing;
minutelyStatBean.pingMin = minutelyData.minPing;
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
// For migration mode, we don't need to store old hourly and minutely data, but we need 24-hour's minutely data
// Run anyway for non-migration mode
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statMinutelyKeepHour, "hour"))) {
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
minutelyStatBean.up = minutelyData.up;
minutelyStatBean.down = minutelyData.down;
minutelyStatBean.ping = minutelyData.avgPing;
minutelyStatBean.pingMin = minutelyData.minPing;
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(minutelyStatBean);
}
await R.store(minutelyStatBean);
// Remove the old data
log.debug("uptime-calc", "Remove old data");
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getMinutelyKey(date.subtract(24, "hour")),
]);
// No need to remove old data in migration mode
if (!this.migrationMode) {
// Remove the old data
// TODO: Improvement: Convert it to a job?
log.debug("uptime-calc", "Remove old data");
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
]);
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getHourlyKey(date.subtract(30, "day")),
]);
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day")),
]);
}
return date;
}
@@ -812,6 +837,14 @@ class UptimeCalculator {
return dayjs.utc();
}
/**
* For migration purposes.
* @param {boolean} value Migration mode on/off
* @returns {void}
*/
setMigrationMode(value) {
this.migrationMode = value;
}
}
class UptimeDataResult {

View File

@@ -115,6 +115,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@@ -552,4 +553,5 @@ const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const Monitor = require("./model/monitor");

View File

@@ -0,0 +1,22 @@
const ColumnCompilerMySQL = require("knex/lib/dialects/mysql/schema/mysql-columncompiler");
const { formatDefault } = require("knex/lib/formatter/formatterUtils");
const { log } = require("../../../../../../../src/util");
class KumaColumnCompiler extends ColumnCompilerMySQL {
/**
* Override defaultTo method to handle default value for TEXT fields
* @param {any} value Value
* @returns {string|void} Default value (Don't understand why it can return void or string, but it's the original code, lol)
*/
defaultTo(value) {
if (this.type === "text" && typeof value === "string") {
log.debug("defaultTo", `${this.args[0]}: ${this.type} ${value} ${typeof value}`);
// MySQL 8.0 is required and only if the value is written as an expression: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
// MariaDB 10.2 is required: https://mariadb.com/kb/en/text/
return `default (${formatDefault(value, this.type, this.client)})`;
}
return super.defaultTo.apply(this, arguments);
}
}
module.exports = KumaColumnCompiler;

View File

@@ -0,0 +1,84 @@
const express = require("express");
const http = require("node:http");
const { log } = require("../../src/util");
/**
* SimpleMigrationServer
* For displaying the migration status of the server
* Also, it is used to let Docker healthcheck know the status of the server, as the main server is not started yet, healthcheck will think the server is down incorrectly.
*/
class SimpleMigrationServer {
/**
* Express app instance
* @type {?Express}
*/
app;
/**
* Server instance
* @type {?Server}
*/
server;
/**
* Response object
* @type {?Response}
*/
response;
/**
* Start the server
* @param {number} port Port
* @param {string} hostname Hostname
* @returns {Promise<void>}
*/
start(port, hostname) {
this.app = express();
this.server = http.createServer(this.app);
this.app.get("/", (req, res) => {
res.set("Content-Type", "text/plain");
res.write("Migration is in progress, listening message...\n");
if (this.response) {
this.response.write("Disconnected\n");
this.response.end();
}
this.response = res;
// never ending response
});
return new Promise((resolve) => {
this.server.listen(port, hostname, () => {
if (hostname) {
log.info("migration", `Migration server is running on http://${hostname}:${port}`);
} else {
log.info("migration", `Migration server is running on http://localhost:${port}`);
}
resolve();
});
});
}
/**
* Update the message
* @param {string} msg Message to update
* @returns {void}
*/
update(msg) {
this.response?.write(msg + "\n");
}
/**
* Stop the server
* @returns {Promise<void>}
*/
async stop() {
this.response?.write("Finished, please refresh this page.\n");
this.response?.end();
await this.server?.close();
}
}
module.exports = {
SimpleMigrationServer,
};

View File

@@ -619,7 +619,7 @@ $shadow-box-padding: 20px;
bottom: 0;
margin-left: -$shadow-box-padding;
margin-right: -$shadow-box-padding;
z-index: 100;
z-index: 10;
background-color: rgba(white, 0.2);
backdrop-filter: blur(2px);
border-radius: 0 0 10px 10px;

View File

@@ -73,12 +73,6 @@ export default {
}
},
created() {
if (this.model.length === 0) {
this.addCondition();
}
},
methods: {
getNewGroup() {
return {

View File

@@ -4,11 +4,17 @@
<div
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
class="beat-hover-area"
:class="{ 'empty': (beat === 0) }"
:style="beatHoverAreaStyle"
:title="getBeatTitle(beat)"
/>
>
<div
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
/>
</div>
</div>
<div
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
@@ -47,7 +53,7 @@ export default {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 4,
beatHoverAreaPadding: 4,
move: false,
maxBeat: -1,
};
@@ -123,7 +129,7 @@ export default {
barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2);
let width = -(this.beatWidth + this.beatHoverAreaPadding * 2);
return {
transition: "all ease-in-out 0.25s",
@@ -137,12 +143,17 @@ export default {
},
beatHoverAreaStyle() {
return {
padding: this.beatHoverAreaPadding + "px",
"--hover-scale": this.hoverScale,
};
},
beatStyle() {
return {
width: this.beatWidth + "px",
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
};
},
@@ -152,7 +163,7 @@ export default {
*/
timeStyle() {
return {
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
"margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px",
};
},
@@ -219,20 +230,20 @@ export default {
if (this.size !== "big") {
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
this.beatHoverAreaPadding = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
const actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio;
if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (!Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
if (!Number.isInteger(actualHoverAreaPadding)) {
this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);
@@ -245,7 +256,7 @@ export default {
*/
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
}
},
@@ -273,32 +284,41 @@ export default {
}
.hp-bar-big {
.beat {
.beat-hover-area {
display: inline-block;
background-color: $primary;
border-radius: $border-radius;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;
transform: scale(var(--hover-scale));
}
.beat {
background-color: $primary;
border-radius: $border-radius;
/*
pointer-events needs to be changed because
tooltip momentarily disappears when crossing between .beat-hover-area and .beat
*/
pointer-events: none;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
}
}
}

View File

@@ -118,6 +118,7 @@ export default {
"clicksendsms": "ClickSend SMS",
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
"discord": "Discord",
"Elks": "46elks",
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
"gotify": "Gotify",
@@ -162,8 +163,10 @@ export default {
"ZohoCliq": "ZohoCliq",
"SevenIO": "SevenIO",
"whapi": "WhatsApp (Whapi)",
"waha": "WhatsApp (WAHA)",
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",
"SendGrid": "SendGrid"
};
// Put notifications here if it's not supported in most regions or its documentation is not in English
@@ -179,8 +182,10 @@ export default {
"SMSManager": "SmsManager (smsmanager.cz)",
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"PushPlus": "PushPlus (推送加)",
"smsc": "SMSC",
"WPush": "WPush(wpush.cn)",
"YZJ": "YZJ (云之家自定义机器人)"
};
// Sort by notification name

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