mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 22:06:59 +08:00
Compare commits
1683 Commits
1.0.8
...
ansible-un
Author | SHA1 | Date | |
---|---|---|---|
|
c742153d4d | ||
|
4aa4c5b853 | ||
|
2e7231edd1 | ||
|
c25b4cf9c8 | ||
|
a5d2dbf620 | ||
|
10220ec5bc | ||
|
a6de002eda | ||
|
df4c354e46 | ||
|
7af628211e | ||
|
bb43dc2825 | ||
|
a1b20698be | ||
|
6d6cb2ad49 | ||
|
cb76801b85 | ||
|
2c0e22ad31 | ||
|
f05651d235 | ||
|
aa92727a61 | ||
|
56dfa05642 | ||
|
71492aeb3a | ||
|
5ee5ea909d | ||
|
a09b97f778 | ||
|
e0a08e6b5d | ||
|
6f5cbbdf69 | ||
|
34ee342d3e | ||
|
f793aa5264 | ||
|
728485d686 | ||
|
cb3429d3c7 | ||
|
0d69b4426e | ||
|
8bb8b0a53c | ||
|
a4841eb8aa | ||
|
2ef2a42e87 | ||
|
9473cd6919 | ||
|
74f18a2b3f | ||
|
f9cd0eb084 | ||
|
6a845bd937 | ||
|
c91f517121 | ||
|
7899707582 | ||
|
12215af2f4 | ||
|
d4bfe57b79 | ||
|
dcc91d6c72 | ||
|
a041a7964a | ||
|
76611ecaca | ||
|
f802154456 | ||
|
9fb461976d | ||
|
c8e364911f | ||
|
88bc08e7b7 | ||
|
03aeab0421 | ||
|
f331f1a63e | ||
|
d645e29455 | ||
|
b4507f9706 | ||
|
fc6d0d1fca | ||
|
b62f1475ee | ||
|
d47d8517a8 | ||
|
19d2db6c8c | ||
|
5a8162747c | ||
|
220108ebc6 | ||
|
984a3704e0 | ||
|
909412c87e | ||
|
481fd3a05f | ||
|
5434e2da4f | ||
|
b3d348dcea | ||
|
0aca0455ab | ||
|
8f3ef734bc | ||
|
120eb0d85f | ||
|
4aaed0837e | ||
|
60657132c0 | ||
|
76cbef85d5 | ||
|
e17ef02008 | ||
|
f33d55c92d | ||
|
67849a9e84 | ||
|
ee79a34148 | ||
|
d2f0480889 | ||
|
c36190bba6 | ||
|
4b3fae53d4 | ||
|
4dd60cba3d | ||
|
4bc84d2122 | ||
|
a796f80018 | ||
|
40cb22e671 | ||
|
d95258e7db | ||
|
baae4b5a5e | ||
|
c1b118a0f6 | ||
|
9c5466890e | ||
|
bf8dbd78b3 | ||
|
6cd130de38 | ||
|
a864b72e03 | ||
|
5070927478 | ||
|
bedc1f8617 | ||
|
077f3837d9 | ||
|
aea128a85b | ||
|
c50b2b636a | ||
|
a284703d9e | ||
|
64ec766423 | ||
|
186c11540f | ||
|
4d947d9374 | ||
|
4888c97d86 | ||
|
50593f3edf | ||
|
c1267e9b3b | ||
|
2ca7a5b962 | ||
|
9f0c66d775 | ||
|
a1f9a82537 | ||
|
37e6ca8d77 | ||
|
0b0fd6609d | ||
|
3a32fd6f42 | ||
|
97cb060cf5 | ||
|
5afb29f8f9 | ||
|
f9b8dbf4db | ||
|
92a5f18bf5 | ||
|
dce908a07b | ||
|
4155f84eec | ||
|
94ffeeeab6 | ||
|
3d222ac5f5 | ||
|
c811c1ccde | ||
|
bd3d34400d | ||
|
5d3bf68123 | ||
|
1f77526210 | ||
|
88ed965d69 | ||
|
7f4d5a0f76 | ||
|
df813fbdee | ||
|
07742799ed | ||
|
f65cc655c0 | ||
|
1a218aaa17 | ||
|
369cad90c1 | ||
|
f9bb48de13 | ||
|
74d2b38cb6 | ||
|
7bba4fe2d0 | ||
|
be3a791e6e | ||
|
9747048890 | ||
|
d5d957b748 | ||
|
5cdb5edeb3 | ||
|
73c18b6ff0 | ||
|
567ea346fe | ||
|
453f6fbadf | ||
|
dd79042128 | ||
|
583e6bf978 | ||
|
b1fca7c1a7 | ||
|
19dd11d624 | ||
|
42ce34b6c7 | ||
|
b7a9d1474f | ||
|
31fa67452e | ||
|
9ef3727c91 | ||
|
ed39485af9 | ||
|
daef238a70 | ||
|
4cc433166e | ||
|
28f530394e | ||
|
b0615d347b | ||
|
be19336149 | ||
|
94508cae2f | ||
|
265cca9ed1 | ||
|
267654c987 | ||
|
2c85491ee0 | ||
|
5d836cf05d | ||
|
ba46fb6b1c | ||
|
5df34cd137 | ||
|
bf64095cea | ||
|
2333d1c7a7 | ||
|
95bae8289d | ||
|
45f7c647a6 | ||
|
dff1056bb1 | ||
|
62222c0336 | ||
|
8f7ca1f4db | ||
|
9a36e227a3 | ||
|
733d0af75f | ||
|
b88e74fad8 | ||
|
734762b773 | ||
|
0275d7a42b | ||
|
41a6d1b701 | ||
|
34d8984e3a | ||
|
c92153c97e | ||
|
ad82ab0305 | ||
|
f952d283c6 | ||
|
e164fabf81 | ||
|
bc69a331ee | ||
|
e4506963d9 | ||
|
222540898b | ||
|
baf3612ece | ||
|
8f44b9f618 | ||
|
210566c7af | ||
|
0481a241f3 | ||
|
179ca232bc | ||
|
0dcb7aed21 | ||
|
23736549f9 | ||
|
665c263c03 | ||
|
c5e6628803 | ||
|
3a1d8ddc11 | ||
|
bc5f61b3ec | ||
|
314fa18bdc | ||
|
57389fab2c | ||
|
ee2c54cfd1 | ||
|
82cde7c847 | ||
|
1ba2034701 | ||
|
dee131c25d | ||
|
e5d6410caf | ||
|
e496c3b3be | ||
|
69f5112b38 | ||
|
c094dc0c5b | ||
|
1fb9b25d13 | ||
|
9a135deac2 | ||
|
8e6173c05e | ||
|
dec84282ed | ||
|
df80f413b5 | ||
|
17e59f1d8d | ||
|
973c2bb429 | ||
|
da0eaddeb8 | ||
|
b2bc8d9db9 | ||
|
541068ff3b | ||
|
83ee46454a | ||
|
75b21c905f | ||
|
60e12f4bfa | ||
|
191b81ee07 | ||
|
f3651a1219 | ||
|
12ef9f39c5 | ||
|
4004926e64 | ||
|
4d3d6d6e25 | ||
|
d06e5ef6fa | ||
|
b12b848d97 | ||
|
bb96a577ca | ||
|
8840ca618b | ||
|
8ec858fd14 | ||
|
124c98ce76 | ||
|
61135e8500 | ||
|
08a58dec2b | ||
|
741ed548da | ||
|
52d80d3a5d | ||
|
586c748d44 | ||
|
b5d6e96b1d | ||
|
68b74f07e4 | ||
|
bc615c2dd8 | ||
|
e7104737e7 | ||
|
1dbf1c3dea | ||
|
74688e69aa | ||
|
b32bfb3ff1 | ||
|
24664cde2c | ||
|
348c5ec995 | ||
|
9143b73f84 | ||
|
5e6d945095 | ||
|
ba93129b18 | ||
|
cf548df15f | ||
|
caa2a34177 | ||
|
69aa60d1fb | ||
|
eaecd6e571 | ||
|
f2a27a2cf1 | ||
|
d4c9431142 | ||
|
d7f7dba13f | ||
|
3e5ae00d25 | ||
|
5311bef3eb | ||
|
c400595f67 | ||
|
e84f7dac60 | ||
|
67a22399bc | ||
|
947fc6001e | ||
|
c87e67ad1b | ||
|
6f92774a8f | ||
|
6e76ab7426 | ||
|
b2fbd7e263 | ||
|
199e6ec82b | ||
|
18a99c2016 | ||
|
e261a27ebe | ||
|
de5cce9d90 | ||
|
b85c9186f9 | ||
|
eb22ad5ffe | ||
|
f5f4835b74 | ||
|
44c1b336dc | ||
|
110ec491ee | ||
|
640b6e5b1c | ||
|
234fba3978 | ||
|
1285ccb537 | ||
|
6c542edfc9 | ||
|
767807dd22 | ||
|
546402f3d2 | ||
|
698a38e773 | ||
|
71884cf42a | ||
|
d676c782bb | ||
|
dd773aa5a2 | ||
|
2852e59ffb | ||
|
cb3da50e7e | ||
|
f25653d778 | ||
|
2e7ad1b7b2 | ||
|
e0e1ab6fa6 | ||
|
8d984881c9 | ||
|
955f9ae20a | ||
|
a9e319517a | ||
|
39ad8b4bb7 | ||
|
8fb8cbdaf3 | ||
|
3f3d8b4eb3 | ||
|
9123e9461f | ||
|
77addfebc8 | ||
|
16846c7c6d | ||
|
d1c4d13903 | ||
|
7cd4bfc11d | ||
|
1d5c0502ab | ||
|
a1cda93ad5 | ||
|
3bd420f0e0 | ||
|
78424b4f2d | ||
|
f8055ed03d | ||
|
fe4724fc53 | ||
|
7f0dda6a44 | ||
|
43791ee97e | ||
|
6362ef6a9c | ||
|
9d3a4e9d1e | ||
|
6c60096f56 | ||
|
ba1e025353 | ||
|
a41a081727 | ||
|
a5f15f2319 | ||
|
e69799f613 | ||
|
3c795bebe3 | ||
|
3a43fec666 | ||
|
24c645e437 | ||
|
9d31da1fe8 | ||
|
2e4c42941a | ||
|
4fc2603818 | ||
|
7bc38d4231 | ||
|
daad63d70b | ||
|
9ddd2c7365 | ||
|
fa5ba12e14 | ||
|
85053f865e | ||
|
1239f6d1a2 | ||
|
fed611d1b9 | ||
|
bc68088350 | ||
|
90200958cd | ||
|
aa13d74d7a | ||
|
d82f305f6e | ||
|
7c63cbfd84 | ||
|
c7e1267779 | ||
|
5d0b54c292 | ||
|
b50b390048 | ||
|
65158cb06b | ||
|
8fe5e4e605 | ||
|
ab5ddae2ee | ||
|
89c64f4ea2 | ||
|
4ccff95d9c | ||
|
40a1ebecc5 | ||
|
e1793596fe | ||
|
c489058a57 | ||
|
95342ec006 | ||
|
bdebbf8e40 | ||
|
9a9fca67d5 | ||
|
17a572112c | ||
|
91649e7956 | ||
|
665bae0806 | ||
|
e4be28a9e7 | ||
|
445674aacb | ||
|
2f7b60f5e5 | ||
|
b83c59e308 | ||
|
ce852dfa02 | ||
|
c9549c0de2 | ||
|
957c292307 | ||
|
b5eb17ed93 | ||
|
d578300104 | ||
|
b77b33e790 | ||
|
4becb97a5d | ||
|
85e2b36424 | ||
|
abdf1ae90a | ||
|
606c967985 | ||
|
93c231b4d9 | ||
|
c42c985e9e | ||
|
177a9598ea | ||
|
133def93fe | ||
|
90ebf4f66c | ||
|
e0e5f3518a | ||
|
a81cc92b07 | ||
|
d6f79ee80b | ||
|
f0632f32ee | ||
|
9ad8e5f56a | ||
|
8a481a1be0 | ||
|
657987a013 | ||
|
d74577608b | ||
|
20a399c557 | ||
|
060dde9827 | ||
|
1d1601cf24 | ||
|
ff5f2e8dfb | ||
|
5451fb7672 | ||
|
56094a43d7 | ||
|
68bbe8944a | ||
|
8f1da6aa22 | ||
|
c0d6fe0d76 | ||
|
29e4e41215 | ||
|
7a1bb964e9 | ||
|
3fe0e9bf1e | ||
|
9982887783 | ||
|
c31efc0ef4 | ||
|
6463d4b209 | ||
|
acada8028a | ||
|
d0b0c64b81 | ||
|
cd04ac4557 | ||
|
d7d2f7b7fc | ||
|
5a05d135b8 | ||
|
e03ee593e2 | ||
|
6c1ee70e15 | ||
|
5c3892313e | ||
|
c57c94642c | ||
|
62f168a2a5 | ||
|
c808f78f09 | ||
|
9c80e1c732 | ||
|
acc2995d86 | ||
|
7def9dcec7 | ||
|
a35569481d | ||
|
9ddffc0f7f | ||
|
76e7c8b276 | ||
|
572a5300aa | ||
|
e1f1d4a959 | ||
|
c6fc385289 | ||
|
c645658161 | ||
|
182597944d | ||
|
8eaa8116c3 | ||
|
3512faad14 | ||
|
f11417e854 | ||
|
b5857f7c0c | ||
|
6277babf25 | ||
|
5f36d2acda | ||
|
cc36ff5210 | ||
|
c363d3374e | ||
|
65a8cb5307 | ||
|
f74b2662c5 | ||
|
300a95d779 | ||
|
23714ab688 | ||
|
16b44001e7 | ||
|
f2f8f33b86 | ||
|
6e18f39eb4 | ||
|
68d44dd9b3 | ||
|
20d59e5a13 | ||
|
ae31eb6ba9 | ||
|
df4682d19b | ||
|
11a1f35cc5 | ||
|
2a3ce15328 | ||
|
7cb25255bf | ||
|
c622f7958f | ||
|
df5efcc71c | ||
|
4cd66b20b1 | ||
|
1276102c18 | ||
|
6944b35ea7 | ||
|
88757ebbbe | ||
|
0a73b84ae6 | ||
|
15f36f96c3 | ||
|
edcaf93446 | ||
|
dec175d55f | ||
|
9a60f69f66 | ||
|
53a008ae2b | ||
|
1d63dd9ddd | ||
|
61627545a5 | ||
|
176fa6b60d | ||
|
cb43ecb46e | ||
|
2e24312f67 | ||
|
6ff3cb275e | ||
|
9d364b28b1 | ||
|
bc3e3f9118 | ||
|
0f3ab7b1d8 | ||
|
8cb26d2b31 | ||
|
d94fbede32 | ||
|
76e619c066 | ||
|
4e4f94ab98 | ||
|
ed3a558397 | ||
|
a419aa527f | ||
|
4d26825cbe | ||
|
7276f34d90 | ||
|
e1eeb44e7f | ||
|
f4b8da0a5c | ||
|
4178983df3 | ||
|
7ac0ab2e34 | ||
|
cd211a6be7 | ||
|
4e71ab7406 | ||
|
76c68071f1 | ||
|
bda481c61e | ||
|
8242a1586d | ||
|
c593a962c2 | ||
|
c9b4d2ae2a | ||
|
37105d720b | ||
|
3b74b727f2 | ||
|
2f0119bc3f | ||
|
a7d2a34dae | ||
|
60acb91fc8 | ||
|
f51156f18e | ||
|
8338881927 | ||
|
674b387c95 | ||
|
5ff9a64e5e | ||
|
4bee57ea7f | ||
|
f75c9e4f0c | ||
|
8ab4788f80 | ||
|
4e4ab0577e | ||
|
6e04ec436e | ||
|
2d471a5e84 | ||
|
cae194f58f | ||
|
655ccc86b9 | ||
|
e2dbacb383 | ||
|
89b34b5748 | ||
|
86dcc9bc8f | ||
|
9b05e86c25 | ||
|
145b722aec | ||
|
2ff7c4de5d | ||
|
79c81395bc | ||
|
178e5cd2c0 | ||
|
84507268ad | ||
|
843992c410 | ||
|
33f773fcd0 | ||
|
26841a64f0 | ||
|
89c0f8b734 | ||
|
57a76e6129 | ||
|
dc805cff97 | ||
|
3fe3450533 | ||
|
330cd6e058 | ||
|
a2f2253221 | ||
|
1e5ce92917 | ||
|
0d7c2960b0 | ||
|
82343de972 | ||
|
521d57c483 | ||
|
281671b938 | ||
|
30d8aadf12 | ||
|
2939bd4138 | ||
|
dcf15c3eb7 | ||
|
7c9ed98408 | ||
|
4b9f0a3fe6 | ||
|
5b67fec084 | ||
|
407581ee07 | ||
|
11c3c636e0 | ||
|
911d4ea37b | ||
|
4039c6549e | ||
|
d733ec018e | ||
|
97fe7c001c | ||
|
03b07730d3 | ||
|
dbc87d8ab3 | ||
|
05b691d4c9 | ||
|
029d6412da | ||
|
9f12f95cce | ||
|
c1112a32df | ||
|
efc78acfeb | ||
|
9e01959d15 | ||
|
3fe91c52cb | ||
|
5269dcec60 | ||
|
a2cc7d1db9 | ||
|
de6437e494 | ||
|
5db728841b | ||
|
18c5a16783 | ||
|
166c4d6b5f | ||
|
12d3aeb0cd | ||
|
2538bd04ce | ||
|
b7528b9a4e | ||
|
9c058054b9 | ||
|
c6683e2a9b | ||
|
b558708be2 | ||
|
fd0dd2d284 | ||
|
1bc77a06e5 | ||
|
69c623ac2b | ||
|
69ffee55dd | ||
|
d769c4426c | ||
|
ebf0671fef | ||
|
97af09fd50 | ||
|
5b19e3f025 | ||
|
ce2df137e6 | ||
|
6d9b71c054 | ||
|
a433de74e6 | ||
|
8e3f43d60b | ||
|
2a1fd93444 | ||
|
dc1de50a02 | ||
|
e223e826a3 | ||
|
0e6d7694ce | ||
|
503d1f0a91 | ||
|
11bcd1e2ed | ||
|
06310423f4 | ||
|
e127e168b6 | ||
|
b5b391c73b | ||
|
075535ba46 | ||
|
13cf6891ac | ||
|
ad0cde6554 | ||
|
25d18f0da3 | ||
|
e9445bb2e3 | ||
|
ecc25ba596 | ||
|
e5286b0973 | ||
|
4e94cb9aad | ||
|
037fdd73a3 | ||
|
bb9a936658 | ||
|
5445c2a2ff | ||
|
dc08510e72 | ||
|
62805014df | ||
|
c79e80442a | ||
|
f0ff96afd9 | ||
|
8083368a81 | ||
|
afb75e07d5 | ||
|
efd3822930 | ||
|
2adac64c83 | ||
|
cee225bcb2 | ||
|
8958c21736 | ||
|
2286f78f57 | ||
|
d9eab90a69 | ||
|
4ba2025451 | ||
|
272d4bde45 | ||
|
0550ceb6d4 | ||
|
82131f4dd2 | ||
|
0c88e4b2f6 | ||
|
5c6230da58 | ||
|
d6753b8833 | ||
|
93a021d027 | ||
|
54f864a1bb | ||
|
d6f7be9112 | ||
|
5137c80c07 | ||
|
792f3c7c5c | ||
|
8a739af5ad | ||
|
edb75808d8 | ||
|
56ae6f6117 | ||
|
5e3ea3293c | ||
|
5c89562650 | ||
|
1f7f20526f | ||
|
0f5b437015 | ||
|
ac80631bcd | ||
|
8caf47988c | ||
|
6fe014fa5e | ||
|
6cf2eb036d | ||
|
9d7def93a5 | ||
|
dca5a59dbc | ||
|
656a4d6270 | ||
|
6dd0e082b4 | ||
|
2e95e2016d | ||
|
4169127143 | ||
|
cac0a46bac | ||
|
d71d27220b | ||
|
fba4f86552 | ||
|
e023ddf1c2 | ||
|
23a2d33f8c | ||
|
b8093e909b | ||
|
c3c273f9df | ||
|
daab2a05f5 | ||
|
a15e9077fc | ||
|
8431a25a3a | ||
|
e8cc7ff771 | ||
|
5d617012a3 | ||
|
d7eac1a413 | ||
|
c589bd836d | ||
|
5ce09953e2 | ||
|
fc8d1e78b6 | ||
|
3f26327f95 | ||
|
efb3f2b19c | ||
|
db791c880a | ||
|
cdda182311 | ||
|
a1c2a1bc52 | ||
|
432b156fce | ||
|
01812cc446 | ||
|
dfd63386ba | ||
|
11abc1f1e0 | ||
|
288e87bb3d | ||
|
79ee0e1ef4 | ||
|
8ae79ab9bf | ||
|
12b5489eb5 | ||
|
ddad2dcb4a | ||
|
e96121f69a | ||
|
5b4af550fb | ||
|
dd183e2ec2 | ||
|
0fcb310b97 | ||
|
3a0143ac46 | ||
|
2ce5c28ed4 | ||
|
ec4b7e4064 | ||
|
5b758a4e98 | ||
|
7907c07034 | ||
|
adfe640f42 | ||
|
469f7a3e32 | ||
|
9f1e7b0a88 | ||
|
cdf81a36d3 | ||
|
bf4ac0cf17 | ||
|
c0846124c2 | ||
|
3d30ed3d3b | ||
|
deec15c09e | ||
|
3423cb5d8e | ||
|
20af179a82 | ||
|
3c60800eab | ||
|
34586d7b8f | ||
|
2c19aef4dc | ||
|
67a623be18 | ||
|
e5f6d7f047 | ||
|
b69550f5b9 | ||
|
d08a71ab49 | ||
|
ed67803af8 | ||
|
a6c839709c | ||
|
5eb3c6b194 | ||
|
8233f3b875 | ||
|
8be4bf0e16 | ||
|
cccf393ee5 | ||
|
9f5bf37a96 | ||
|
18e4702375 | ||
|
1eb3f63a82 | ||
|
8c63536eb8 | ||
|
3e1788983e | ||
|
a8badb027d | ||
|
0c6b434d79 | ||
|
b5bd92ce78 | ||
|
3f80cf5e54 | ||
|
162ef04c41 | ||
|
a87595a849 | ||
|
7626e1f2e4 | ||
|
7f1edb49bc | ||
|
d184733af9 | ||
|
704d63b49f | ||
|
7002a778f0 | ||
|
54d2fbcc02 | ||
|
fbd4d54812 | ||
|
c8706b9aa1 | ||
|
54d7830813 | ||
|
fef26f3d5e | ||
|
cb263f2a08 | ||
|
52102d72a0 | ||
|
2b00e59c7a | ||
|
8ea7a693a1 | ||
|
4ded0c073a | ||
|
7a8b6a03e0 | ||
|
6bebc623f9 | ||
|
34b86352f2 | ||
|
99e8a33118 | ||
|
d7cc585101 | ||
|
8c357a04bf | ||
|
044c78aa2d | ||
|
49eaa1a166 | ||
|
215cc07907 | ||
|
22227be408 | ||
|
5decfb9fad | ||
|
f9d7b99367 | ||
|
359fe52c2e | ||
|
bc4db6c692 | ||
|
f14a798b2c | ||
|
a0ffa42b42 | ||
|
550825927c | ||
|
c1501742f5 | ||
|
7c98fe603e | ||
|
b7ae49c644 | ||
|
edad2caf8e | ||
|
2bf6a04f81 | ||
|
f0670dde20 | ||
|
73068763c0 | ||
|
73bf1216d1 | ||
|
98436f91b5 | ||
|
49720c709c | ||
|
842d359ad3 | ||
|
e71f5bf314 | ||
|
79e0c9e1f1 | ||
|
e6ff957d9d | ||
|
865b721b79 | ||
|
b5d987863d | ||
|
911690bea8 | ||
|
d25603e629 | ||
|
259bcf9426 | ||
|
afeb424dc0 | ||
|
6b44116245 | ||
|
aa478af286 | ||
|
5f8d0faacd | ||
|
707e05c330 | ||
|
4da63c5fb8 | ||
|
8ae64843fc | ||
|
23e64b8efd | ||
|
8c55a8bf98 | ||
|
387a8919f9 | ||
|
a1edc23b1d | ||
|
7ee89fab5c | ||
|
980342546e | ||
|
6b60dc9630 | ||
|
8d22b43f24 | ||
|
dad58341c6 | ||
|
275902be38 | ||
|
38213585f3 | ||
|
37d1e50ff1 | ||
|
a2a4c70cf5 | ||
|
446fc1af0b | ||
|
51acd107e3 | ||
|
d3517e76c1 | ||
|
3f0b85e5a8 | ||
|
2625cbe0d2 | ||
|
c93f42794f | ||
|
668fd58af3 | ||
|
b7568e9caa | ||
|
1c2adf8723 | ||
|
96129921e9 | ||
|
2b1fe815f9 | ||
|
fcf017d5c7 | ||
|
843830a38a | ||
|
ee830621dd | ||
|
c2a560e2ed | ||
|
13bdfefa9d | ||
|
3d6c8b7f05 | ||
|
2aaed66b38 | ||
|
7b4c70860c | ||
|
6513c3e75f | ||
|
7fa1cb83af | ||
|
9d079eeec0 | ||
|
21dd5ad3dd | ||
|
3394e1f148 | ||
|
ee22406301 | ||
|
8d5eaaf8a7 | ||
|
b246c8e0f2 | ||
|
1ed4ac9494 | ||
|
0f2059cde0 | ||
|
138ddf5608 | ||
|
dcd68213b1 | ||
|
9e95d568c2 | ||
|
fa3da819f8 | ||
|
9e1f1f006b | ||
|
55cb497301 | ||
|
8d8d5987e7 | ||
|
1fa90bffaa | ||
|
67f221d3c7 | ||
|
05f28fecb2 | ||
|
ba4a4aaf1c | ||
|
6eceb4c744 | ||
|
3e4154dfb5 | ||
|
fbc8828ddc | ||
|
f2c7308c96 | ||
|
3677aa639f | ||
|
aaddfa1786 | ||
|
6d65d248f4 | ||
|
87a4748b40 | ||
|
182bdf13a7 | ||
|
0f66e5cfc5 | ||
|
fe5ae46013 | ||
|
efbadd0737 | ||
|
f9d633e02b | ||
|
756f317f82 | ||
|
fa9d26416c | ||
|
58aa83331e | ||
|
cc9fe26584 | ||
|
99818aa370 | ||
|
682265fe9c | ||
|
4406e51ab6 | ||
|
c3d5be5a5e | ||
|
dfe12c99c1 | ||
|
bb69851160 | ||
|
6c9323351d | ||
|
8e9fa20e57 | ||
|
da7c29f4b9 | ||
|
b67b4d5afd | ||
|
9f06d54688 | ||
|
1448de7b19 | ||
|
e545d48583 | ||
|
47749ca58d | ||
|
04b7a4a423 | ||
|
56d8f585fd | ||
|
07c9d78829 | ||
|
15c4a8fb02 | ||
|
f41e95921f | ||
|
647184e5d1 | ||
|
e60426bdcd | ||
|
8d84d8f891 | ||
|
74acc2cea7 | ||
|
ba4a4089eb | ||
|
251d42f1a6 | ||
|
122631c91b | ||
|
1e12f25c4b | ||
|
9e10296290 | ||
|
87e213085f | ||
|
d9038f1da2 | ||
|
3fb2d0ce68 | ||
|
50a33e3b45 | ||
|
f24abac7fc | ||
|
ce9a97a107 | ||
|
0afa3a2c21 | ||
|
51512b6f5f | ||
|
3d79e841c9 | ||
|
d2af42e579 | ||
|
d6e3bd2282 | ||
|
70fbe9a2d9 | ||
|
c669e7eaba | ||
|
9a9fd41f62 | ||
|
50b868e751 | ||
|
a856780066 | ||
|
266b03fbf7 | ||
|
662c97dcde | ||
|
1ebf752f1a | ||
|
69bb6ef290 | ||
|
720ea850e1 | ||
|
7fb55b8875 | ||
|
4786514e9f | ||
|
32c9dfbb31 | ||
|
d3d4363031 | ||
|
6f352a6e3c | ||
|
3fb06a8ba4 | ||
|
b3a5a5b0ba | ||
|
a4cad3db65 | ||
|
5fa9b33c79 | ||
|
f6a984b671 | ||
|
23a63213aa | ||
|
a9bb8ae6a1 | ||
|
27d4c3c194 | ||
|
439f45d91e | ||
|
66598a81cc | ||
|
45a6f364e1 | ||
|
95391be2ab | ||
|
624f632a7a | ||
|
5f533b9091 | ||
|
29920f6b60 | ||
|
6e9d12638c | ||
|
6e55c44773 | ||
|
ad1bb93730 | ||
|
0a5a6e6a4b | ||
|
fe0fc63843 | ||
|
5b2e1f6086 | ||
|
8c7ee94769 | ||
|
0834770509 | ||
|
a71814c80b | ||
|
17073fd786 | ||
|
15c00d9158 | ||
|
d2b34f9285 | ||
|
a96a515087 | ||
|
2eab919ae0 | ||
|
c09b67b94d | ||
|
601204ae77 | ||
|
61c737c53c | ||
|
2820118eba | ||
|
469e8f6fd6 | ||
|
780cb0a145 | ||
|
8c941b1d56 | ||
|
1c8b3ce451 | ||
|
e8ef63e0a3 | ||
|
4591adc05e | ||
|
5f6aa32844 | ||
|
a8e170f6a8 | ||
|
6aaf984f29 | ||
|
76a39f1388 | ||
|
34c0fa59a8 | ||
|
0664217a09 | ||
|
b0e9c5bcb4 | ||
|
0b572df3d0 | ||
|
ad6fcc2f2e | ||
|
bcc02c1680 | ||
|
795d5f586f | ||
|
7ee98d989c | ||
|
7dc1e84e44 | ||
|
6350c43cc3 | ||
|
fd95d41d9f | ||
|
ffbc25722d | ||
|
7665bae927 | ||
|
09e38269c6 | ||
|
6681f49a58 | ||
|
3fc2ba3d76 | ||
|
78e12ab899 | ||
|
2b8c049e7b | ||
|
f0ac3c82d2 | ||
|
803029a9e4 | ||
|
c3f1cebeab | ||
|
5d9814559c | ||
|
9ef45a9c7e | ||
|
7f78cc8d0f | ||
|
ca84044809 | ||
|
9eaa4ab846 | ||
|
c3122a9807 | ||
|
f6498caa9a | ||
|
3a0bc80016 | ||
|
2fb3c40307 | ||
|
66e40d9fcb | ||
|
cbbc503f8e | ||
|
de8b61ef2b | ||
|
534ac4b720 | ||
|
e9735d239b | ||
|
5be51abd8f | ||
|
6ec219908b | ||
|
a6fdd272a6 | ||
|
1b5e723f60 | ||
|
4bdada36a9 | ||
|
250c2bdd6d | ||
|
8e49d84050 | ||
|
112d72da47 | ||
|
9b8f01cfc6 | ||
|
579e07ded4 | ||
|
a04878fa84 | ||
|
2955abb5d9 | ||
|
8230cfe13f | ||
|
8b463e70df | ||
|
392f8275b3 | ||
|
783ec97004 | ||
|
4f4fe39c9b | ||
|
611d214a32 | ||
|
7a0cebf5bb | ||
|
54aa68ec58 | ||
|
217637aa1f | ||
|
ab22961538 | ||
|
8ba8de07ae | ||
|
a34b2623c8 | ||
|
79920b5f2c | ||
|
72783fd94c | ||
|
80322cbfe7 | ||
|
7e0272077b | ||
|
b2ff6b6098 | ||
|
512ff09cca | ||
|
e8f4fabcd0 | ||
|
f679c1cbaf | ||
|
2ab06f87b8 | ||
|
76db55b657 | ||
|
1693873f4a | ||
|
53bfdb7dad | ||
|
db05b506f3 | ||
|
0752f810f1 | ||
|
200d233607 | ||
|
7274913686 | ||
|
1300448bed | ||
|
3f67326fce | ||
|
8b48f9bd22 | ||
|
76348cae5d | ||
|
0b32adadb8 | ||
|
3425a27a0e | ||
|
fff5fd8e9e | ||
|
1d6670ed9a | ||
|
795411a11c | ||
|
3234aec5b3 | ||
|
afe91078c4 | ||
|
2009f7097d | ||
|
c14b71a4a7 | ||
|
35fe9690e8 | ||
|
ad2062713c | ||
|
379656335d | ||
|
fc9b4617ca | ||
|
40d301457a | ||
|
9902c181bc | ||
|
069c811af8 | ||
|
f9311e4e7f | ||
|
34abff4724 | ||
|
d7a230ac15 | ||
|
9da2a01a74 | ||
|
74da02da2c | ||
|
97360dab26 | ||
|
56dad006b5 | ||
|
47092258f8 | ||
|
a0838543b9 | ||
|
ccb8736b3d | ||
|
f351b423c5 | ||
|
ab6b97d77a | ||
|
0e16e88fa8 | ||
|
c746923018 | ||
|
df8754827d | ||
|
2c02dad1f9 | ||
|
600ec6d171 | ||
|
728636de06 | ||
|
8be7fd5c45 | ||
|
59ff9fa293 | ||
|
a654e409df | ||
|
76f1f34a0a | ||
|
35aca46b68 | ||
|
29d0db805d | ||
|
6a925c7ed4 | ||
|
8b9b86b478 | ||
|
7c4b5f189b | ||
|
a6f9762c4d | ||
|
a0e4e96160 | ||
|
fcbeed55bf | ||
|
9b5abf9bb1 | ||
|
73f4b8e177 | ||
|
e6669fbb9e | ||
|
e66a2d362d | ||
|
c2148fc0f1 | ||
|
6e3a904aaa | ||
|
50175b733c | ||
|
2617e1f4d8 | ||
|
91ee39ec60 | ||
|
4711aeb355 | ||
|
db9e97d859 | ||
|
7199bb5ead | ||
|
bb87972543 | ||
|
ea723c7595 | ||
|
e205adfd7b | ||
|
063d64eec8 | ||
|
5abf2e7597 | ||
|
7c83bed273 | ||
|
1cc788da68 | ||
|
54ffef8b7a | ||
|
44aa837a9d | ||
|
f47f7758f9 | ||
|
ddcd3c2a19 | ||
|
7df9698e5d | ||
|
471333ad03 | ||
|
d313966d80 | ||
|
8205f90f3d | ||
|
02247c4174 | ||
|
8352d9abbe | ||
|
2d7767c905 | ||
|
c52b9b63d5 | ||
|
e55193270d | ||
|
9d39071a91 | ||
|
389d247463 | ||
|
a170694a83 | ||
|
5969e913b5 | ||
|
0d280f5edb | ||
|
660b969178 | ||
|
c26622aa39 | ||
|
a7674755ba | ||
|
6d7cb44acc | ||
|
400fc13cf2 | ||
|
db14768229 | ||
|
3d27561e5a | ||
|
b80c88d9a0 | ||
|
862672731f | ||
|
7fee4a7ea7 | ||
|
c4f78d776e | ||
|
f8f9f59464 | ||
|
934685637a | ||
|
295ccba44b | ||
|
8cd5bad44c | ||
|
d003abcd60 | ||
|
f6d1a82989 | ||
|
651b525d06 | ||
|
66769e1c79 | ||
|
3e25f0e9d9 | ||
|
39c7b5e748 | ||
|
b709857e19 | ||
|
9e10f7d35f | ||
|
6caae725f9 | ||
|
c387fb264e | ||
|
4b0a8087a2 | ||
|
08c7a37052 | ||
|
0969be5981 | ||
|
2ed8f12dc0 | ||
|
2da77d8448 | ||
|
f8322de5da | ||
|
e21a18a593 | ||
|
14b7688b70 | ||
|
d953ba7c60 | ||
|
ef1604675b | ||
|
83fc844e8e | ||
|
22a7acbaf6 | ||
|
201bba63d7 | ||
|
0d6888c586 | ||
|
a06fc14213 | ||
|
31b4a5c33e | ||
|
951ece8a65 | ||
|
a49df29a87 | ||
|
08de0090dc | ||
|
97df200d6c | ||
|
d5b17a3778 | ||
|
2f0d994c83 | ||
|
e683033f4e | ||
|
c3c576bd13 | ||
|
c62732fa9c | ||
|
d823493fad | ||
|
463c385cf1 | ||
|
59cccf8c50 | ||
|
403202d4d4 | ||
|
2583dbe0e0 | ||
|
ca479673e7 | ||
|
573c7faddd | ||
|
7a4432de1e | ||
|
94a6c1a344 | ||
|
ee9e48fe53 | ||
|
a4aee49b03 | ||
|
e330875c80 | ||
|
44eba70b78 | ||
|
c6dab944d1 | ||
|
0f4bc5850b | ||
|
f37190a73d | ||
|
29f96fae93 | ||
|
331ae5ec20 | ||
|
8ee34c7904 | ||
|
4f07c2ea9a | ||
|
0b1d0d22ff | ||
|
24facc79d7 | ||
|
9f9c1007d7 | ||
|
70a6f0313c | ||
|
1d05df6ec9 | ||
|
b54bbc302d | ||
|
dd283423ab | ||
|
6006038689 | ||
|
a7b50c3630 | ||
|
0ddbac5109 | ||
|
0f440596c8 | ||
|
5a0fcebd6e | ||
|
87678ea92d | ||
|
0bf0cfa104 | ||
|
a7cf14c663 | ||
|
325ce72a71 | ||
|
9d00bd9029 | ||
|
f6a168282e | ||
|
acf6f1883b | ||
|
f44f951e40 | ||
|
dea9b05d49 | ||
|
230a9bfaf9 | ||
|
d761d54d0e | ||
|
e37621853e | ||
|
f2cec60481 | ||
|
2212cf9c08 | ||
|
f7562e00c1 | ||
|
678e248966 | ||
|
c1aebef005 | ||
|
1ef4562905 | ||
|
69a7c4dab4 | ||
|
2c3880a326 | ||
|
f4e885982d | ||
|
dcf6dd8b32 | ||
|
093cfec8dd | ||
|
f13dcb3c5b | ||
|
1a5ffd4755 | ||
|
a2f9e9e9c4 | ||
|
62712f5cc4 | ||
|
f0f1847ad8 | ||
|
0aeaf87f5b | ||
|
5ca0dd628d | ||
|
4a106431f3 | ||
|
d164b6ccce | ||
|
da74391c3e | ||
|
850563442a | ||
|
242e494cb5 | ||
|
4faa409027 | ||
|
f842fb409e | ||
|
72dd894856 | ||
|
0b0417e064 | ||
|
fe35d95441 | ||
|
da131a5156 | ||
|
926c15ea40 | ||
|
87e874406a | ||
|
0e288ea92d | ||
|
8a4a87716f | ||
|
d01fa9bcfc | ||
|
ec8c1cad31 | ||
|
fb0fa2a843 | ||
|
f5c6d422d5 | ||
|
ea4240455f | ||
|
ce30ee74e9 | ||
|
66b5f157eb | ||
|
5fe8a0917a | ||
|
848296b77a | ||
|
edfaacbb5f | ||
|
bb8385d690 | ||
|
b95404b6e0 | ||
|
cc351b2883 | ||
|
3c06f249ca | ||
|
31648dc5e8 | ||
|
1197cfa11e | ||
|
fd8c95d64e | ||
|
58240aceef | ||
|
e8b814733d | ||
|
dcc7799108 | ||
|
424f20f8e1 | ||
|
d4ff5d8b32 | ||
|
899b33b3a9 | ||
|
1b8b33c4c3 | ||
|
d34ed00841 | ||
|
f9c177b150 | ||
|
9952463350 | ||
|
5837c353b7 | ||
|
d5b32ffbb8 | ||
|
61936ae5eb | ||
|
299506ce45 | ||
|
ca8d4ab61b | ||
|
ffbdf97478 | ||
|
cc25787878 | ||
|
17453a8812 | ||
|
f1a151b4a1 | ||
|
d35b205fcc | ||
|
2a34e41d8c | ||
|
41d32bb9dd | ||
|
0b9e410ea5 | ||
|
904ed326ca | ||
|
778995a4fb | ||
|
ee60d74910 | ||
|
6a603203cc | ||
|
4b8e7fcffc | ||
|
7fd12f5485 | ||
|
0d87a6dfea | ||
|
b0acda52f9 | ||
|
e9cd9be03a | ||
|
6ae279c7f3 | ||
|
9c32adfb55 | ||
|
d346afd33b | ||
|
3bf380c684 | ||
|
dca5c59982 | ||
|
8f9a973ede | ||
|
b98ec0c3d4 | ||
|
2082c3f68a | ||
|
ea9f434553 | ||
|
fa1216c198 | ||
|
6d8aa20fc6 | ||
|
5430da955a | ||
|
e6e2b0ebf8 | ||
|
445dc486be | ||
|
d2151737c1 | ||
|
78fc6de542 | ||
|
c15e6631ae | ||
|
ebf362754c | ||
|
f84135ca67 | ||
|
80eca886ce | ||
|
274a9050c0 | ||
|
1f4ad938b1 | ||
|
312aedf391 | ||
|
4f28bfbde3 | ||
|
ee76ab4ec2 | ||
|
18616ee590 | ||
|
19a4d570ec | ||
|
79fda8f442 | ||
|
53a14cf4f5 | ||
|
2241d8817f | ||
|
3831dfe0b9 | ||
|
fdde862549 | ||
|
e31be8caf5 | ||
|
60f2f08cea | ||
|
b1647a310e | ||
|
23f1a73fc8 | ||
|
d2bf2a551d | ||
|
7d70c4d8cd | ||
|
532ad3044c | ||
|
f23ecef636 | ||
|
51cf2ff6f9 | ||
|
b30b1d3a52 | ||
|
582e14098d | ||
|
6e3e2fc85c | ||
|
b604807cfe | ||
|
3ee13bddd1 | ||
|
c74986647e | ||
|
dac410c850 | ||
|
b88b357b55 | ||
|
9a5cd5172b | ||
|
941788db49 | ||
|
99725aabe7 | ||
|
2dd392e609 | ||
|
9d41da4aa2 | ||
|
a0f372e946 | ||
|
877ad1438f | ||
|
9116654a33 | ||
|
dbc70bc5ed | ||
|
a29b65e801 | ||
|
bd9568ce5b | ||
|
c13cc62d3d | ||
|
7a109689d9 | ||
|
e7929f461d | ||
|
a2cf7f394e | ||
|
e1f378ee6c | ||
|
eeb00a5511 | ||
|
8e1e4b9204 | ||
|
66f9ad5754 | ||
|
129dc3324b | ||
|
268d2e2353 | ||
|
7d2cf0ee57 | ||
|
2d408732db | ||
|
0cc5053f14 | ||
|
1c4e5b79be | ||
|
4aae402b36 | ||
|
b604910bbb | ||
|
2f6c5963c5 | ||
|
dce2ba8f9f | ||
|
ed5c75282c | ||
|
00ac560bd6 | ||
|
8ea4dec5a0 | ||
|
3006d13aee | ||
|
c7e6cb9f37 | ||
|
9b82bb248e | ||
|
e55cf65f28 | ||
|
f24002838c | ||
|
b4fd4f7d90 | ||
|
3a3f1762ac | ||
|
6376d4eb92 | ||
|
e37cf9b1a7 | ||
|
3b0191210b | ||
|
923d325b44 | ||
|
ad38e61c26 | ||
|
22095c09b1 | ||
|
a5e62141d5 | ||
|
e4b76717be | ||
|
bc70ecfba7 | ||
|
f1238ab762 | ||
|
cd1a3a2fb9 | ||
|
25db162721 | ||
|
7b92166d18 | ||
|
1341d220ed | ||
|
3e7bb355fd | ||
|
697fa6bdfd | ||
|
527e0c3444 | ||
|
a41534ca60 | ||
|
fa549cb80e | ||
|
0127d5102e | ||
|
ec731d174d | ||
|
0d65918a6a | ||
|
21db31bb30 | ||
|
3ee0addb1f | ||
|
8c5d1945be | ||
|
bb799163e8 | ||
|
bf29f28726 | ||
|
22db9c9b8a | ||
|
96ff70faaa | ||
|
2776f942ab | ||
|
14b9afb400 | ||
|
3ad736692f | ||
|
1952e34110 | ||
|
e644a1e36f | ||
|
78b7e36a38 | ||
|
f30232c35d | ||
|
257a4ee994 | ||
|
fd1ba46d3d | ||
|
ada6606217 | ||
|
dd877cfc70 | ||
|
93edb8817d | ||
|
858affa808 | ||
|
2bb182b2b4 | ||
|
303adbf9b1 | ||
|
712da02324 | ||
|
27a4dbb722 | ||
|
0e676060f2 | ||
|
a14c40f9bb | ||
|
309fbfa094 | ||
|
46dcd31142 | ||
|
59e9315647 | ||
|
91e82bd12c | ||
|
d4dd650bfe | ||
|
354ee1a58c | ||
|
2901a38628 | ||
|
6464207f4b | ||
|
7652b4849a | ||
|
03b4086372 | ||
|
acd5cf63fd | ||
|
177af2d588 | ||
|
b46b214175 | ||
|
d2f0a15076 | ||
|
2d50d24276 | ||
|
34b6976a03 | ||
|
d60c11e845 | ||
|
890aea8756 | ||
|
c55f2b93f7 | ||
|
d4cf838af7 | ||
|
508586fcfd | ||
|
feb0feda76 | ||
|
34ca5c5c15 | ||
|
2b8c5e2e65 | ||
|
44d9967cfb | ||
|
8318c2e8ff | ||
|
46ac753c46 | ||
|
72740ba477 | ||
|
5d438ca2b6 | ||
|
02a12e68b8 | ||
|
a7cd70f7de | ||
|
26db0471da | ||
|
d313a06d5c | ||
|
4c1f2f85f8 | ||
|
23851ef539 | ||
|
397fd12081 | ||
|
564bc96735 | ||
|
682e4d45e2 | ||
|
f96d792fa1 | ||
|
2d20634738 | ||
|
9442c3fa05 | ||
|
302d2665d2 | ||
|
2b68be52b0 | ||
|
393c4fb1a7 | ||
|
13f6c79e79 | ||
|
ca69d06e0d | ||
|
d8d428907e | ||
|
a17c14ea1c | ||
|
28a51d806b | ||
|
6eead46fa6 | ||
|
44d9fa63f0 | ||
|
dd4c00eed3 | ||
|
752ac05149 | ||
|
14652c9b5f | ||
|
054186370e | ||
|
40d80dfdfd | ||
|
b9684e32d3 | ||
|
8e726da82a | ||
|
df41c40cc2 | ||
|
5dc834794c | ||
|
36ace3e56c | ||
|
3f12afce28 | ||
|
833b4733a8 | ||
|
066b67dfce | ||
|
b2041cb36b | ||
|
aa2233eb2d | ||
|
e5981b10ce | ||
|
46cb955172 | ||
|
50f300dd28 | ||
|
2f50fc4c00 | ||
|
a02edf1b4f | ||
|
a61a65e5ae | ||
|
ffaaeae794 | ||
|
ee3bf2961c | ||
|
ce79f8bfc7 | ||
|
c79be19ec3 | ||
|
b892a92fc8 | ||
|
2912ca1248 | ||
|
2bff1ebe0f | ||
|
ec0dbf3cbe | ||
|
210a0d414c | ||
|
ca38cc91e9 | ||
|
c90d17bd66 | ||
|
dbd3f48f68 | ||
|
49ba5fb1b2 | ||
|
d27789a8ae | ||
|
b53582a812 | ||
|
05680472a7 | ||
|
ca3b0a0f19 | ||
|
4571a9b8c1 | ||
|
362eabab8d | ||
|
a22d0f6951 | ||
|
b1168d4cdb | ||
|
6fcc2253ec | ||
|
f21937b197 | ||
|
1e7623c459 | ||
|
22047fe932 | ||
|
209e44c2e1 | ||
|
30b8d3d0ab | ||
|
64498163e1 | ||
|
4f70a70dda | ||
|
5b5a32967c | ||
|
ae8b5eea5a | ||
|
b761aaffdf | ||
|
7ffdb2eb80 | ||
|
2d36f4cd4a | ||
|
2339405f90 | ||
|
8f5e5ad944 | ||
|
575c3ee182 | ||
|
c9aa110f6c | ||
|
bb0af35d47 | ||
|
61944d642e | ||
|
21640e1bbe | ||
|
de4515ea6e | ||
|
b41799f801 | ||
|
d8bcfcaaa2 | ||
|
cf5168a4e6 | ||
|
60b0ee2959 | ||
|
f1e5e53e8f | ||
|
432388a905 | ||
|
746c1b6acc | ||
|
269ac2410b | ||
|
f72cdcc663 | ||
|
6b3fbcd1e7 | ||
|
8a48f5dd71 | ||
|
d218661f3d | ||
|
77369bd002 | ||
|
29a89df524 | ||
|
e257fa7b2d | ||
|
6980c38a6c | ||
|
8d57df7256 | ||
|
64501bf065 | ||
|
440c178403 | ||
|
c9c51e47e1 | ||
|
5e52f230b1 | ||
|
61e758d872 | ||
|
86826fb826 | ||
|
7a32e5e6ff | ||
|
610f2f9c47 | ||
|
01e9c76a6f | ||
|
5927c2703f | ||
|
316db89b9a | ||
|
eed6d3e847 | ||
|
2a62f6daae | ||
|
e09c296410 | ||
|
d7f660ec57 | ||
|
798f39acf0 | ||
|
31d5b4fd3d | ||
|
fc76c2836b | ||
|
0b30bfff87 | ||
|
72f0724b9a | ||
|
35176a614f | ||
|
8e883c9c6a | ||
|
2f89ee4937 | ||
|
5d0b6190c3 | ||
|
cb85905c33 | ||
|
233c5661af | ||
|
91d4c15b4d | ||
|
981ed5f29f | ||
|
0b45694f2f | ||
|
60531d0b15 | ||
|
a3de63ac3c | ||
|
80eadcb236 | ||
|
7e5a8c896b | ||
|
efe75bde75 | ||
|
af34e861c5 | ||
|
2ae2022e62 | ||
|
37f1d60f82 | ||
|
d39b43dacc | ||
|
7ca80fc086 | ||
|
eb34dc6cc2 | ||
|
ed93aae1c2 | ||
|
e1a38f64f8 | ||
|
6a8ccf627a | ||
|
8f150aaeb9 | ||
|
6ed1d8cb2f | ||
|
71bec74081 | ||
|
2bd735035c | ||
|
48c6d8f19f | ||
|
2d176a38af | ||
|
b14f63491d | ||
|
24b87fcd5a | ||
|
45c162583b | ||
|
365ea0a189 | ||
|
2461f5084e | ||
|
1d0b332b42 | ||
|
d5149f90b4 | ||
|
e0ae9a9e73 | ||
|
3227a2660b | ||
|
764160f38c | ||
|
70e7945a66 | ||
|
b413427a37 | ||
|
debcac4924 | ||
|
268dd33792 | ||
|
692a11e51e | ||
|
5eb4f55dfd | ||
|
e7cc5340e5 | ||
|
4d4d504d6e | ||
|
2a4695a774 | ||
|
f089bf73c3 | ||
|
f099e4270d | ||
|
81636c7b44 | ||
|
98fa995d3f | ||
|
42d24258cf | ||
|
3f56167198 | ||
|
5163e16482 | ||
|
d93f6e2716 | ||
|
d6fad7f1ef | ||
|
5512b15162 | ||
|
8979311653 | ||
|
4f058c5b47 | ||
|
9ba1743900 | ||
|
1e4f9c7e15 | ||
|
974672f7c1 | ||
|
01ac6d54be | ||
|
113899e278 | ||
|
d1d000bd74 | ||
|
ef4677a640 | ||
|
e39c46ff9b | ||
|
0e46ce42d1 | ||
|
efc9a254f4 | ||
|
116d803592 | ||
|
ba1d271afa | ||
|
12910b23ed | ||
|
550c9703a6 | ||
|
b69185ee9e | ||
|
ddcfa558f7 | ||
|
478d2c4e8c | ||
|
1352a0a162 | ||
|
7274b82143 | ||
|
69b1454cf5 | ||
|
8f2a9fe883 | ||
|
1b8476417d | ||
|
2a65402ad8 | ||
|
59ef1f13db | ||
|
bf33f97c9e | ||
|
d0aad3400c | ||
|
6f489e7e0f | ||
|
f9cb8293f3 | ||
|
11b8c61079 | ||
|
f69ba12c10 | ||
|
e78cfaa492 | ||
|
9c17f59fe8 | ||
|
519add4fab | ||
|
46c7e5d058 | ||
|
6291b7b8bb | ||
|
3fb515e871 | ||
|
8e440f7dff | ||
|
6d58c98b24 | ||
|
6ca7ca4e7e | ||
|
44391117ab | ||
|
9fa8d5c1fa | ||
|
3265c3cbc3 | ||
|
b3721e03a8 | ||
|
4ff68238c4 | ||
|
7b1000d995 | ||
|
a79e6aa338 | ||
|
3005585c0f | ||
|
123fca43a1 | ||
|
d5b40dfebf | ||
|
c990edc87d | ||
|
2677f5dd87 | ||
|
4469b3a19b | ||
|
ebf207c2f5 | ||
|
a50aa93e84 | ||
|
91fce75a93 | ||
|
3a7414125a | ||
|
5a6e5b7948 | ||
|
adcd251076 | ||
|
dadc270876 | ||
|
a98ba41c8e | ||
|
a40816b948 | ||
|
d3e24df225 | ||
|
908176c910 | ||
|
9ade9af1e2 | ||
|
8350bff629 | ||
|
93ea2c277a | ||
|
6251f47050 | ||
|
8f7885e58a | ||
|
dffe3cf8f2 | ||
|
d411143f3c | ||
|
a03dd91e40 | ||
|
2c2ac9dc59 | ||
|
d06711a1a7 | ||
|
94f2219715 | ||
|
d315e8306b | ||
|
8cd0e7a058 | ||
|
8fce62632d | ||
|
38c0c170e7 | ||
|
655536e457 | ||
|
807db8a2d8 | ||
|
d707eba046 | ||
|
e34a8e2e4a | ||
|
6bd9d85a9a | ||
|
f2de6299f6 | ||
|
a28d6eafae | ||
|
fce0edebc9 | ||
|
221aad55de | ||
|
377d475e05 | ||
|
0c3c59df4e | ||
|
eba996b0f2 | ||
|
b9a6088f50 |
@@ -1,11 +0,0 @@
|
||||
spec:
|
||||
name: uptime-kuma
|
||||
services:
|
||||
- name: server
|
||||
git:
|
||||
repo_clone_url: https://github.com/louislam/uptime-kuma
|
||||
branch: master
|
||||
http_port: 3001
|
||||
build_command: npm run setup
|
||||
run_command: npm run start-server
|
||||
|
@@ -1,35 +0,0 @@
|
||||
/.idea
|
||||
/dist
|
||||
/node_modules
|
||||
/data
|
||||
/.do
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/docker-compose*
|
||||
**/[Dd]ockerfile*
|
||||
LICENSE
|
||||
README.md
|
||||
.editorconfig
|
||||
.vscode
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.github
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
app.json
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
#node_modules
|
||||
.DS_Store
|
||||
#dist
|
||||
dist-ssr
|
||||
*.local
|
||||
#.idea
|
||||
|
||||
#/data
|
||||
#!/data/.gitkeep
|
||||
#.vscode
|
||||
|
||||
### End of .gitignore content
|
@@ -1,21 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.vue]
|
||||
trim_trailing_whitespace = false
|
73
.eslintrc.js
73
.eslintrc.js
@@ -1,73 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@babel/eslint-parser",
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
},
|
||||
rules: {
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
"no-unused-vars": "warn",
|
||||
indent: [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
ignoredNodes: ["TemplateLiteral"],
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
quotes: ["warn", "double"],
|
||||
//semi: ['off', 'never'],
|
||||
"vue/html-indent": ["warn", 4], // default: 2
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"no-multi-spaces": ["error", {
|
||||
ignoreEOLComments: true,
|
||||
}],
|
||||
"curly": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-curly-newline": "off",
|
||||
"object-property-newline": "error",
|
||||
"comma-spacing": "error",
|
||||
"brace-style": "error",
|
||||
"no-var": "error",
|
||||
"key-spacing": "warn",
|
||||
"keyword-spacing": "warn",
|
||||
"space-infix-ops": "warn",
|
||||
"arrow-spacing": "warn",
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-constant-condition": ["error", {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
"space-before-blocks": "warn",
|
||||
//'no-console': 'warn',
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-multiple-empty-lines": ["warn", {
|
||||
"max": 1,
|
||||
"maxBOF": 0,
|
||||
}],
|
||||
"lines-between-class-members": ["warn", "always", {
|
||||
exceptAfterSingleLine: true,
|
||||
}],
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-else-return": ["error", {
|
||||
"allowElseIf": false,
|
||||
}],
|
||||
"array-bracket-newline": ["error", "consistent"],
|
||||
"eol-last": ["error", "always"],
|
||||
//'prefer-template': 'error',
|
||||
"comma-dangle": ["warn", "only-multiline"],
|
||||
},
|
||||
}
|
10
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
10
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Ask for help
|
||||
about: You can ask any question related to Uptime Kuma.
|
||||
title: ''
|
||||
labels: help
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Uptime Kuma Version:
|
||||
- Using Docker?: Yes/No
|
||||
- OS:
|
||||
- Browser:
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@@ -1,71 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '35 5 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.idea
|
||||
|
||||
/data
|
||||
!/data/.gitkeep
|
||||
.vscode
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "stylelint-config-recommended",
|
||||
}
|
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
louis@uptimekuma.louislam.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
104
CONTRIBUTING.md
104
CONTRIBUTING.md
@@ -1,104 +0,0 @@
|
||||
# Project Info
|
||||
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
|
||||
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
|
||||
|
||||
# Project Styles
|
||||
|
||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||
- All settings in frontend.
|
||||
- Easy to use
|
||||
|
||||
# Tools
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports .editorconfig (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
|
||||
# Prepare the dev
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
|
||||
```bash
|
||||
npm run start-server
|
||||
|
||||
# Or
|
||||
|
||||
node server/server.js
|
||||
|
||||
```
|
||||
|
||||
It binds to 0.0.0.0:3001 by default.
|
||||
|
||||
|
||||
## Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
|
||||
# Frontend Dev
|
||||
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
|
||||
|
||||
You can use Vue Devtool Chrome extension for debugging.
|
||||
|
||||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
|
||||
|
||||
```javascript
|
||||
localStorage.dev = "dev";
|
||||
```
|
||||
|
||||
So that the frontend will try to connect websocket server in 3001.
|
||||
|
||||
Alternately, you can specific NODE_ENV to "development".
|
||||
|
||||
|
||||
## Build the frontend
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Frontend Details
|
||||
|
||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||
|
||||
The router in "src/main.js"
|
||||
|
||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
||||
|
||||
The data and socket logic in "src/mixins/socket.js"
|
||||
|
||||
# Database Migration
|
||||
|
||||
TODO
|
||||
|
||||
# Unit Test
|
||||
|
||||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.
|
||||
|
||||
|
||||
|
||||
|
||||
|
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Louis Lam
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
137
README.md
137
README.md
@@ -1,137 +0,0 @@
|
||||
# Uptime Kuma
|
||||
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a>
|
||||
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
|
||||
|
||||
# Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
|
||||
* 20 seconds interval.
|
||||
|
||||
# How to Use
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Create a volume
|
||||
docker volume create uptime-kuma
|
||||
|
||||
# Start the container
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
|
||||
Change Port and Volume
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
## Without Docker
|
||||
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/louislam/uptime-kuma.git
|
||||
cd uptime-kuma
|
||||
npm run setup
|
||||
|
||||
# Option 1. Try it
|
||||
npm run start-server
|
||||
|
||||
# (Recommended)
|
||||
# Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have: npm install pm2 -g
|
||||
pm2 start npm --name uptime-kuma -- run start-server
|
||||
|
||||
# Listen to different port or hostname
|
||||
pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0.0.0
|
||||
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
|
||||
|
||||
## (Optional) One more step for Reverse Proxy
|
||||
|
||||
This is optional for someone who want to do reverse proxy.
|
||||
|
||||
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket.
|
||||
|
||||
Please read wiki for more info:
|
||||
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
|
||||
|
||||
## One-click Deploy
|
||||
|
||||
<!---
|
||||
Abort. Heroku instance killed the server.js if idle, stupid.
|
||||
[](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.8)
|
||||
-->
|
||||
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
||||
|
||||
# How to Update
|
||||
|
||||
### Docker
|
||||
|
||||
Re-pull the latest docker image and create another container with the same volume.
|
||||
|
||||
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
|
||||
|
||||
### Without Docker
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git checkout 1.0.8 --force
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart uptime-kuma
|
||||
```
|
||||
|
||||
# What's Next?
|
||||
|
||||
I will mark requests/issues to the next milestone.
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
|
||||
# More Screenshots
|
||||
|
||||
Settings Page:
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/2.jpg" width="400" alt="" />
|
||||
|
||||
Telegram Notification Sample:
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
|
||||
|
||||
|
||||
# Motivation
|
||||
|
||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
|
||||
* Want to build a fancy UI.
|
||||
* Learn Vue 3 and vite.js.
|
||||
* Show the power of Bootstrap 5.
|
||||
* Try to use WebSocket with SPA instead of REST API.
|
||||
* Deploy my first Docker image to Docker Hub.
|
||||
|
||||
|
||||
If you love this project, please consider giving me a ⭐.
|
||||
|
||||
|
||||
# Contribute
|
||||
|
||||
If you want to report a bug or request a new feature. Free feel to open a new issue.
|
||||
|
||||
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
||||
English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki.
|
1
ansible/.gitignore
vendored
Normal file
1
ansible/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
roles/nginx/files/ssl/*
|
39
ansible/README.md
Normal file
39
ansible/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Ansible Playbook to install uptime kuma using docker
|
||||
|
||||
This playbook comes with three tags
|
||||
|
||||
1. requirements (will install anything needed to make next parts working)
|
||||
2. docker (to install docker)
|
||||
3. nginx (to install nginx using docker with ssl)
|
||||
4. uptime kuma (to install uptime kuma using docker)
|
||||
|
||||
To see more info see docker-compose, tasks and config files
|
||||
I will try to make this readme better
|
||||
|
||||
## To run it
|
||||
1. install ansible see [here](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
|
||||
2. run `ansible-galaxy install -r ansible-requirements.yml` to get requirements
|
||||
3. prepare inventory hosts
|
||||
4. put your certificates in files section in nginx role with this structure below:
|
||||
```
|
||||
ansible -> roles -> nginx -> files -> ssl -> <uptime kuma domain>.fullchain.pem
|
||||
ansible -> roles -> nginx -> files -> ssl -> <uptime kuma domain>.privkey.pem
|
||||
```
|
||||
5. to run playbook
|
||||
```bash
|
||||
ansible-playbook ./playbook.yml -i <your inventory path> -e "kuma_domain=<uptime kuma domain>" -e "kuma_image_os=<alpine or debian>" -e "kuma_image_version=<version>"
|
||||
```
|
||||
you can use other ansible playbook options too
|
||||
|
||||
> Note: Replace `<uptime kuma domain>` with your desired domain for uptime kuma
|
||||
|
||||
> replace `<version>` with a version from https://github.com/louislam/uptime-kuma/releases
|
||||
> replace `<alpine or debian>` with one of options
|
||||
|
||||
> `-e "kuma_image_os=<alpine or debian>" -e "kuma_image_version=<version>"` is not required and you can remove this part or change only one of them (kuma_image_os is debian & kuma_image_version is 1 by default)
|
||||
|
||||
> If you are not using root user as your ansible_user use -bK option to become root
|
||||
|
||||
> instead of `-e "kuma_image_os=<alpine or debian>" -e "kuma_image_version=<version>"` You can use `-e kuma_tag=<uptime kuma full tag>` and replace `<uptime kuma full tag>` with your desired tag (e.g. `latest`)
|
||||
|
||||
> you can also create a yaml file with variables that you want to set & use it (also: ansible-vars)
|
6
ansible/ansible-requirements.yml
Normal file
6
ansible/ansible-requirements.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
roles:
|
||||
- src: geerlingguy.docker
|
||||
- src: geerlingguy.pip
|
||||
|
||||
collections:
|
||||
- name: community.docker
|
20
ansible/playbook.yml
Normal file
20
ansible/playbook.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
- name: install uptime kuma with nginx connected
|
||||
hosts: all
|
||||
|
||||
vars:
|
||||
pip_install_packages:
|
||||
- name: wheel
|
||||
- name: pip
|
||||
state: latest
|
||||
- name: setuptools
|
||||
- name: cffi
|
||||
- name: docker
|
||||
- name: dockerpty
|
||||
docker_compose_version: "v2.0.1"
|
||||
|
||||
roles:
|
||||
- {role: requirements, tags: ["docker", "requirements"]}
|
||||
- {role: geerlingguy.docker, tags: ["docker"]}
|
||||
- {role: geerlingguy.pip, tags: ["docker"]}
|
||||
- {role: nginx, tags: ["nginx"]}
|
||||
- {role: uptime-kuma, tags: ["kuma"]}
|
22
ansible/roles/nginx/tasks/main.yml
Normal file
22
ansible/roles/nginx/tasks/main.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
- name: Ensure Volumes & Files directories exists
|
||||
file:
|
||||
dest: "{{item}}"
|
||||
state: directory
|
||||
loop:
|
||||
- /compose
|
||||
- /compose/volumes
|
||||
- /compose/volumes/nginx
|
||||
- /compose/volumes/nginx/log/{{ kuma_domain }}
|
||||
|
||||
- name: Ensure nginx SSL certificates exist
|
||||
copy:
|
||||
src: ssl
|
||||
dest: /compose/volumes/nginx
|
||||
mode: 'preserve'
|
||||
group: root
|
||||
owner: root
|
||||
|
||||
- name: Ensure config files are updated
|
||||
template:
|
||||
src: "nginx.conf"
|
||||
dest: /compose/volumes/nginx/nginx.conf
|
88
ansible/roles/nginx/templates/nginx.conf
Normal file
88
ansible/roles/nginx/templates/nginx.conf
Normal file
@@ -0,0 +1,88 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
pid /var/run/nginx.pid;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
events {
|
||||
worker_connections 2048;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
default_type application/octet-stream;
|
||||
|
||||
### SSL Settings for all servers (https://ssl-config.mozilla.org/#server=nginx&server-version=1.17.2&config=intermediate)
|
||||
# certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
|
||||
ssl_certificate /etc/nginx/ssl/{{ kuma_domain }}.fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/{{ kuma_domain }}.privkey.pem;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
|
||||
# intermediate configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/nginx/ssl/dhparam.pem (TODO: check if it's secure to use others DH parameters!)
|
||||
# openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
|
||||
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
|
||||
|
||||
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
# OCSP stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request_method $scheme://$host$request_uri $server_protocol" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" $request_time $upstream_response_time UPA:$upstream_addr BYS:$bytes_sent BYR:$request_length';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
### Set additional headers to be send to upstream
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Remove Headers that gonna be sent to client
|
||||
proxy_hide_header X-Powered-By;
|
||||
proxy_hide_header Server;
|
||||
|
||||
# Redirect HTTP request to HTTPS
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name {{ kuma_domain }};
|
||||
return 302 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name {{ kuma_domain }};
|
||||
listen 443 ssl http2 default_server;
|
||||
|
||||
access_log /var/log/nginx/{{ kuma_domain }}/access.log main;
|
||||
error_log /var/log/nginx/{{ kuma_domain }}/error.log;
|
||||
|
||||
location / {
|
||||
# rewrite ^/(.*)/$ /$1 permanent;
|
||||
### redirect urls with trailing slash to non-trailing slash
|
||||
# https://serverfault.dev/questions/597302/removing-the-trailing-slash-from-a-url-with-nginx
|
||||
# location ~ (?<no_slash>.+)/$ {
|
||||
# return 302 https://$host$no_slash;
|
||||
# }
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://uptime-kuma:3001/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
}
|
11
ansible/roles/requirements/tasks/main.yml
Normal file
11
ansible/roles/requirements/tasks/main.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Ensure {{inventory_hostname}} is set as hostname
|
||||
hostname:
|
||||
name: "{{inventory_hostname}}"
|
||||
tags: ["hostname"]
|
||||
|
||||
- include_tasks: setup-RedHat.yml
|
||||
when: ansible_os_family == 'RedHat'
|
||||
|
||||
- include_tasks: setup-Debian.yml
|
||||
when: ansible_os_family == 'Debian'
|
9
ansible/roles/requirements/tasks/setup-Debian.yml
Normal file
9
ansible/roles/requirements/tasks/setup-Debian.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
- name: Ensure packages for some requirements are installed
|
||||
apt:
|
||||
pkg:
|
||||
- libffi-dev
|
||||
- libzbar-dev
|
||||
- libzbar0
|
||||
- python3-docopt
|
||||
update_cache: yes
|
||||
state: present
|
9
ansible/roles/requirements/tasks/setup-RedHat.yml
Normal file
9
ansible/roles/requirements/tasks/setup-RedHat.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
- name: Ensure packages for some requirements are installed
|
||||
dnf:
|
||||
name:
|
||||
- libffi-devel
|
||||
- zbar-devel
|
||||
- zbar
|
||||
- python3-docopt
|
||||
update_cache: yes
|
||||
state: present
|
4
ansible/roles/uptime-kuma/defaults/main.yml
Normal file
4
ansible/roles/uptime-kuma/defaults/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
kuma_image_version: '1'
|
||||
kuma_image_os: 'debian'
|
||||
kuma_tag: "{{kuma_image_version}}-{{kuma_image_os}}"
|
23
ansible/roles/uptime-kuma/tasks/main.yml
Normal file
23
ansible/roles/uptime-kuma/tasks/main.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
- name: Ensure Volumes & Files directories exists
|
||||
file:
|
||||
dest: "{{item}}"
|
||||
state: directory
|
||||
loop:
|
||||
- /compose
|
||||
- /compose/kuma
|
||||
- /compose/volumes
|
||||
- /compose/volumes/kuma
|
||||
|
||||
- name: Ensure docker-compose file has been updated
|
||||
template:
|
||||
src: "{{item}}"
|
||||
dest: /compose/kuma/
|
||||
loop:
|
||||
- docker-compose.yml
|
||||
|
||||
- name: Ensure uptime-kuma is up
|
||||
community.docker.docker_compose:
|
||||
state: present
|
||||
project_src: /compose/kuma
|
||||
pull: yes
|
||||
recreate: always
|
29
ansible/roles/uptime-kuma/templates/docker-compose.yml
Normal file
29
ansible/roles/uptime-kuma/templates/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
uptime-kuma:
|
||||
restart: always
|
||||
networks:
|
||||
- uptime-kuma
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- '/compose/volumes/uptime-kuma:/app/data'
|
||||
container_name: uptime-kuma
|
||||
image: 'louislam/uptime-kuma:{{kuma_tag}}'
|
||||
|
||||
nginx:
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
networks:
|
||||
- uptime-kuma
|
||||
depends_on:
|
||||
- uptime-kuma
|
||||
restart: always
|
||||
image: nginx:stable-alpine
|
||||
volumes:
|
||||
- '/compose/volumes/nginx/:/etc/nginx/'
|
||||
- '/compose/volumes/nginx/log/{{ kuma_domain }}:/var/log/nginx/{{ kuma_domain }}/'
|
||||
|
||||
networks:
|
||||
uptime-kuma:
|
7
app.json
7
app.json
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "Uptime Kuma",
|
||||
"description": "A fancy self-hosted monitoring tool",
|
||||
"repository": "https://github.com/louislam/uptime-kuma",
|
||||
"logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||
"keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"]
|
||||
}
|
BIN
db/kuma.db
BIN
db/kuma.db
Binary file not shown.
@@ -1,37 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
|
||||
-- SQL Generated by Intellij Idea
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table monitor_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
name VARCHAR(150),
|
||||
active BOOLEAN default 1 not null,
|
||||
user_id INTEGER
|
||||
references user
|
||||
on update cascade on delete set null,
|
||||
interval INTEGER default 20 not null,
|
||||
url TEXT,
|
||||
type VARCHAR(20),
|
||||
weight INTEGER default 2000,
|
||||
hostname VARCHAR(255),
|
||||
port INTEGER,
|
||||
created_date DATETIME,
|
||||
keyword VARCHAR(255)
|
||||
);
|
||||
|
||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
@@ -1,9 +0,0 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE monitor_tls_info (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
info_json TEXT
|
||||
);
|
||||
|
||||
COMMIT;
|
@@ -1,37 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- Add maxretries column to monitor
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table monitor_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
name VARCHAR(150),
|
||||
active BOOLEAN default 1 not null,
|
||||
user_id INTEGER
|
||||
references user
|
||||
on update cascade on delete set null,
|
||||
interval INTEGER default 20 not null,
|
||||
url TEXT,
|
||||
type VARCHAR(20),
|
||||
weight INTEGER default 2000,
|
||||
hostname VARCHAR(255),
|
||||
port INTEGER,
|
||||
created_date DATETIME,
|
||||
keyword VARCHAR(255),
|
||||
maxretries INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
@@ -1,40 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- OK.... serious wrong, missing maxretries column
|
||||
-- Developers should patch it manually if you have missing the maxretries column
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table monitor_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
name VARCHAR(150),
|
||||
active BOOLEAN default 1 not null,
|
||||
user_id INTEGER
|
||||
references user
|
||||
on update cascade on delete set null,
|
||||
interval INTEGER default 20 not null,
|
||||
url TEXT,
|
||||
type VARCHAR(20),
|
||||
weight INTEGER default 2000,
|
||||
hostname VARCHAR(255),
|
||||
port INTEGER,
|
||||
created_date DATETIME,
|
||||
keyword VARCHAR(255),
|
||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||
ignore_tls BOOLEAN default 0 not null,
|
||||
upside_down BOOLEAN default 0 not null
|
||||
);
|
||||
|
||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
@@ -1,13 +0,0 @@
|
||||
# Simple docker-composer.yml
|
||||
# You can change your port or volume location
|
||||
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma
|
||||
container_name: uptime-kuma
|
||||
volumes:
|
||||
- ./uptime-kuma:/app/data
|
||||
ports:
|
||||
- 3001:3001
|
32
dockerfile
32
dockerfile
@@ -1,32 +0,0 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \
|
||||
apk del .build-deps
|
||||
|
||||
# Touching above code may causes sqlite3 re-compile again, painful slow.
|
||||
|
||||
# Install apprise
|
||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
||||
RUN pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
RUN apprise --version
|
||||
|
||||
# New things add here
|
||||
|
||||
COPY . .
|
||||
RUN npm install && \
|
||||
npm run build && \
|
||||
npm prune
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
|
||||
CMD ["npm", "run", "start-server"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
@@ -1,19 +0,0 @@
|
||||
var http = require("http");
|
||||
var options = {
|
||||
host: "localhost",
|
||||
port: "3001",
|
||||
timeout: 2000,
|
||||
};
|
||||
var request = http.request(options, (res) => {
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
request.on("error", function (err) {
|
||||
console.log("ERROR");
|
||||
process.exit(1);
|
||||
});
|
||||
request.end();
|
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* String.prototype.replaceAll() polyfill
|
||||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||
* @author Chris Ferdinandi
|
||||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function(str, newStr){
|
||||
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
||||
return this.replace(str, newStr);
|
||||
}
|
||||
|
||||
// If a string
|
||||
return this.replace(new RegExp(str, 'g'), newStr);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const pkg = require('../package.json');
|
||||
const fs = require("fs");
|
||||
const oldVersion = pkg.version
|
||||
const newVersion = oldVersion + "-nightly"
|
||||
|
||||
console.log("Old Version: " + oldVersion)
|
||||
console.log("New Version: " + newVersion)
|
||||
|
||||
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
|
||||
if (fs.existsSync("README.md")) {
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
||||
}
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* String.prototype.replaceAll() polyfill
|
||||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||
* @author Chris Ferdinandi
|
||||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function(str, newStr){
|
||||
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
||||
return this.replace(str, newStr);
|
||||
}
|
||||
|
||||
// If a string
|
||||
return this.replace(new RegExp(str, 'g'), newStr);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const pkg = require('../package.json');
|
||||
const fs = require("fs");
|
||||
const oldVersion = pkg.version
|
||||
const newVersion = process.argv[2]
|
||||
|
||||
console.log("Old Version: " + oldVersion)
|
||||
console.log("New Version: " + newVersion)
|
||||
|
||||
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
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
||||
}
|
||||
|
16
index.html
16
index.html
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#5cdd8b" />
|
||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
<title>Uptime Kuma</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
7110
package-lock.json
generated
7110
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.0.8",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"update": "",
|
||||
"build": "vite build",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.8 --target release . --push",
|
||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||
"setup": "git checkout 1.0.8 && npm install && npm run build",
|
||||
"version-global-replace": "node extra/version-global-replace.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"command-exists": "^1.2.9",
|
||||
"dayjs": "^1.10.6",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"prom-client": "^13.1.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"redbean-node": "0.0.20",
|
||||
"socket.io": "^4.1.3",
|
||||
"socket.io-client": "^4.1.3",
|
||||
"sqlite3": "^5.0.2",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"vue": "^3.0.5",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-router": "^4.0.10",
|
||||
"vue-toastification": "^2.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.13.10",
|
||||
"@types/bootstrap": "^5.0.17",
|
||||
"@vitejs/plugin-legacy": "^1.5.0",
|
||||
"@vitejs/plugin-vue": "^1.3.0",
|
||||
"@vue/compiler-sfc": "^3.1.5",
|
||||
"core-js": "^3.15.2",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-plugin-vue": "^7.14.0",
|
||||
"sass": "^1.36.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-recommended": "^5.0.0",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.3.5",
|
||||
"vite": "^2.4.4"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB |
BIN
public/icon.png
BIN
public/icon.png
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB |
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@@ -1,51 +0,0 @@
|
||||
const basicAuth = require("express-basic-auth")
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { debug } = require("../src/util");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param username : string
|
||||
* @param password : string
|
||||
* @returns {Promise<Bean|null>}
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
username,
|
||||
])
|
||||
|
||||
if (user && passwordHash.verify(password, user.password)) {
|
||||
// Upgrade the hash to bcrypt
|
||||
if (passwordHash.needRehash(user.password)) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(password),
|
||||
user.id,
|
||||
]);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function myAuthorizer(username, password, callback) {
|
||||
|
||||
setting("disableAuth").then((result) => {
|
||||
|
||||
if (result) {
|
||||
callback(null, true)
|
||||
} else {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
exports.basicAuth = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
@@ -1,120 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const { sleep } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const {
|
||||
setSetting, setting,
|
||||
} = require("./util-server");
|
||||
|
||||
class Database {
|
||||
|
||||
static templatePath = "./db/kuma.db"
|
||||
static path = "./data/kuma.db";
|
||||
static latestVersion = 4;
|
||||
static noReject = true;
|
||||
|
||||
static async patch() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
|
||||
if (! version) {
|
||||
version = 0;
|
||||
}
|
||||
|
||||
console.info("Your database version: " + version);
|
||||
console.info("Latest database version: " + this.latestVersion);
|
||||
|
||||
if (version === this.latestVersion) {
|
||||
console.info("Database no need to patch");
|
||||
} else {
|
||||
console.info("Database patch is needed")
|
||||
|
||||
console.info("Backup the db")
|
||||
const backupPath = "./data/kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, backupPath);
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
console.info(`Patching ${sqlFile}`);
|
||||
await Database.importSQLFile(sqlFile);
|
||||
console.info(`Patched ${sqlFile}`);
|
||||
await setSetting("database_version", i);
|
||||
}
|
||||
console.log("Database Patched Successfully");
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
console.error("Patch db failed!!! Restoring the backup")
|
||||
fs.copyFileSync(backupPath, Database.path);
|
||||
console.error(ex)
|
||||
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
* @param filename
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async importSQLFile(filename) {
|
||||
|
||||
await R.getCell("SELECT 1");
|
||||
|
||||
let text = fs.readFileSync(filename).toString();
|
||||
|
||||
// Remove all comments (--)
|
||||
let lines = text.split("\n");
|
||||
lines = lines.filter((line) => {
|
||||
return ! line.startsWith("--")
|
||||
});
|
||||
|
||||
// Split statements by semicolon
|
||||
// Filter out empty line
|
||||
text = lines.join("\n")
|
||||
|
||||
let statements = text.split(";")
|
||||
.map((statement) => {
|
||||
return statement.trim();
|
||||
})
|
||||
.filter((statement) => {
|
||||
return statement !== "";
|
||||
})
|
||||
|
||||
for (let statement of statements) {
|
||||
await R.exec(statement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async close() {
|
||||
const listener = (reason, p) => {
|
||||
Database.noReject = false;
|
||||
};
|
||||
process.addListener("unhandledRejection", listener);
|
||||
|
||||
console.log("Closing DB")
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
await R.close()
|
||||
await sleep(2000)
|
||||
|
||||
if (Database.noReject) {
|
||||
break;
|
||||
} else {
|
||||
console.log("Waiting to close the db")
|
||||
}
|
||||
}
|
||||
console.log("SQLite closed")
|
||||
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
@@ -1,30 +0,0 @@
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
/**
|
||||
* status:
|
||||
* 0 = DOWN
|
||||
* 1 = UP
|
||||
* 2 = PENDING
|
||||
*/
|
||||
class Heartbeat extends BeanModel {
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
monitorID: this.monitor_id,
|
||||
status: this.status,
|
||||
time: this.time,
|
||||
msg: this.msg,
|
||||
ping: this.ping,
|
||||
important: this.important,
|
||||
duration: this.duration,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Heartbeat;
|
@@ -1,390 +0,0 @@
|
||||
const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
|
||||
const { tcping, ping, checkCertificate } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification")
|
||||
|
||||
/**
|
||||
* status:
|
||||
* 0 = DOWN
|
||||
* 1 = UP
|
||||
* 2 = PENDING
|
||||
*/
|
||||
class Monitor extends BeanModel {
|
||||
async toJSON() {
|
||||
|
||||
let notificationIDList = {};
|
||||
|
||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||
this.id,
|
||||
])
|
||||
|
||||
for (let bean of list) {
|
||||
notificationIDList[bean.notification_id] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: this.active,
|
||||
type: this.type,
|
||||
interval: this.interval,
|
||||
keyword: this.keyword,
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
notificationIDList,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIgnoreTls() {
|
||||
return Boolean(this.ignoreTls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isUpsideDown() {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
let prometheus = new Prometheus(this);
|
||||
|
||||
const beat = async () => {
|
||||
|
||||
if (! previousBeat) {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
this.id,
|
||||
])
|
||||
}
|
||||
|
||||
const isFirstBeat = !previousBeat;
|
||||
|
||||
let bean = R.dispense("heartbeat")
|
||||
bean.monitor_id = this.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = DOWN;
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
bean.status = flipStatus(bean.status);
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (! isFirstBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||
} else {
|
||||
bean.duration = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.type === "http" || this.type === "keyword") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
// Use Custom agent to disable session reuse
|
||||
// https://github.com/nodejs/node/issues/3940
|
||||
let res = await axios.get(this.url, {
|
||||
headers: {
|
||||
"User-Agent": "Uptime-Kuma",
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0,
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
}),
|
||||
});
|
||||
bean.msg = `${res.status} - ${res.statusText}`
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
try {
|
||||
await this.updateTlsInfo(checkCertificate(res));
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
} else {
|
||||
|
||||
let data = res.data;
|
||||
|
||||
// Convert to string for object/array
|
||||
if (typeof data !== "string") {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found"
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but keyword is not found")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
bean.ping = await tcping(this.hostname, this.port);
|
||||
bean.msg = ""
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.msg = ""
|
||||
bean.status = UP;
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
bean.status = flipStatus(bean.status);
|
||||
|
||||
if (bean.status === DOWN) {
|
||||
throw new Error("Flip UP to DOWN");
|
||||
}
|
||||
}
|
||||
|
||||
retries = 0;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
bean.msg = error.message;
|
||||
|
||||
// If UP come in here, it must be upside down mode
|
||||
// Just reset the retries
|
||||
if (this.isUpsideDown() && bean.status === UP) {
|
||||
retries = 0;
|
||||
|
||||
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
])
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up"
|
||||
} else {
|
||||
text = "🔴 Down"
|
||||
}
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
||||
} else if (bean.status === PENDING) {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
|
||||
} else {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||
}
|
||||
|
||||
prometheus.update(bean)
|
||||
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
|
||||
await R.store(bean)
|
||||
Monitor.sendStats(io, this.id, this.user_id)
|
||||
|
||||
previousBeat = bean;
|
||||
}
|
||||
|
||||
beat();
|
||||
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method:
|
||||
* returns URL object for further usage
|
||||
* returns null if url is invalid
|
||||
* @returns {null|URL}
|
||||
*/
|
||||
getUrl() {
|
||||
try {
|
||||
return new URL(this.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateTlsInfo(checkCertificateResult) {
|
||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
this.id,
|
||||
]);
|
||||
if (tls_info_bean == null) {
|
||||
tls_info_bean = R.dispense("monitor_tls_info");
|
||||
tls_info_bean.monitor_id = this.id;
|
||||
}
|
||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
||||
await R.store(tls_info_bean);
|
||||
}
|
||||
|
||||
static async sendStats(io, monitorID, userID) {
|
||||
Monitor.sendAvgPing(24, io, monitorID, userID);
|
||||
Monitor.sendUptime(24, io, monitorID, userID);
|
||||
Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
Monitor.sendCertInfo(io, monitorID, userID);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
let avgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping)
|
||||
FROM heartbeat
|
||||
WHERE time > DATETIME('now', ? || ' hours')
|
||||
AND ping IS NOT NULL
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
monitorID,
|
||||
]));
|
||||
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
static async sendCertInfo(io, monitorID, userID) {
|
||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
monitorID,
|
||||
]);
|
||||
if (tls_info != null) {
|
||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uptime with calculation
|
||||
* Calculation based on:
|
||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
let sec = duration * 3600;
|
||||
|
||||
let heartbeatList = await R.getAll(`
|
||||
SELECT duration, time, status
|
||||
FROM heartbeat
|
||||
WHERE time > DATETIME('now', ? || ' hours')
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
let downtime = 0;
|
||||
let total = 0;
|
||||
let uptime;
|
||||
|
||||
// Special handle for the first heartbeat only
|
||||
if (heartbeatList.length === 1) {
|
||||
|
||||
if (heartbeatList[0].status === 1) {
|
||||
uptime = 1;
|
||||
} else {
|
||||
uptime = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
for (let row of heartbeatList) {
|
||||
let value = parseInt(row.duration)
|
||||
let time = row.time
|
||||
|
||||
// Handle if heartbeat duration longer than the target duration
|
||||
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
|
||||
if (value > sec) {
|
||||
let trim = dayjs.utc().diff(dayjs(time), "second");
|
||||
value = sec - trim;
|
||||
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
total += value;
|
||||
if (row.status === 0 || row.status === 2) {
|
||||
downtime += value;
|
||||
}
|
||||
}
|
||||
|
||||
uptime = (total - downtime) / total;
|
||||
|
||||
if (uptime < 0) {
|
||||
uptime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
@@ -1,378 +0,0 @@
|
||||
const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const FormData = require("form-data");
|
||||
const nodemailer = require("nodemailer");
|
||||
const child_process = require("child_process");
|
||||
|
||||
class Notification {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param notification
|
||||
* @param msg
|
||||
* @param monitorJSON
|
||||
* @param heartbeatJSON
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* Throw Error with fail msg
|
||||
*/
|
||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
if (notification.type === "telegram") {
|
||||
try {
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
params: {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
},
|
||||
})
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
} else if (notification.type === "gotify") {
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
}
|
||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
})
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "webhook") {
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let finalData;
|
||||
let config = {};
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
|
||||
config = {
|
||||
headers: finalData.getHeaders(),
|
||||
}
|
||||
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "smtp") {
|
||||
return await Notification.smtp(notification, msg)
|
||||
|
||||
} else if (notification.type === "discord") {
|
||||
try {
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
username: "Uptime-Kuma",
|
||||
content: msg,
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, data)
|
||||
return okMsg;
|
||||
}
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
var alertColor = "16711680";
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
var alertColor = "65280";
|
||||
}
|
||||
let data = {
|
||||
username: "Uptime-Kuma",
|
||||
embeds: [{
|
||||
title: "Uptime-Kuma Alert",
|
||||
color: alertColor,
|
||||
fields: [
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Message",
|
||||
value: msg,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "signal") {
|
||||
try {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"number": notification.signalNumber,
|
||||
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "slack") {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Slack testing successful.",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"blocks": [{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
}],
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushover") {
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "apprise") {
|
||||
|
||||
return Notification.apprise(notification, msg)
|
||||
|
||||
} else if (notification.type === "lunasea") {
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(lunaseadevice, testdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, downdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 1) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, updata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Notification type is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
static async save(notification, notificationID, userID) {
|
||||
let bean
|
||||
|
||||
if (notificationID) {
|
||||
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||
notificationID,
|
||||
userID,
|
||||
])
|
||||
|
||||
if (! bean) {
|
||||
throw new Error("notification not found")
|
||||
}
|
||||
|
||||
} else {
|
||||
bean = R.dispense("notification")
|
||||
}
|
||||
|
||||
bean.name = notification.name;
|
||||
bean.user_id = userID;
|
||||
bean.config = JSON.stringify(notification)
|
||||
await R.store(bean)
|
||||
}
|
||||
|
||||
static async delete(notificationID, userID) {
|
||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||
notificationID,
|
||||
userID,
|
||||
])
|
||||
|
||||
if (! bean) {
|
||||
throw new Error("notification not found")
|
||||
}
|
||||
|
||||
await R.trash(bean)
|
||||
}
|
||||
|
||||
static async smtp(notification, msg) {
|
||||
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
auth: {
|
||||
user: notification.smtpUsername,
|
||||
pass: notification.smtpPassword,
|
||||
},
|
||||
});
|
||||
|
||||
// send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
text: msg,
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
|
||||
static async apprise(notification, msg) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
}
|
||||
|
||||
throw new Error(output)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require("command-exists").sync;
|
||||
let exists = commandExistsSync("apprise");
|
||||
return exists;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Notification,
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
const passwordHashOld = require("password-hash");
|
||||
const bcrypt = require("bcrypt");
|
||||
const saltRounds = 10;
|
||||
|
||||
exports.generate = function (password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
}
|
||||
|
||||
exports.verify = function (password, hash) {
|
||||
if (isSHA1(hash)) {
|
||||
return passwordHashOld.verify(password, hash)
|
||||
}
|
||||
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
|
||||
function isSHA1(hash) {
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"))
|
||||
}
|
||||
|
||||
exports.needRehash = function (hash) {
|
||||
return isSHA1(hash);
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
||||
// Fixed on Windows
|
||||
|
||||
let spawn = require("child_process").spawn,
|
||||
events = require("events"),
|
||||
fs = require("fs"),
|
||||
WIN = /^win/.test(process.platform),
|
||||
LIN = /^linux/.test(process.platform),
|
||||
MAC = /^darwin/.test(process.platform);
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
function Ping(host, options) {
|
||||
if (!host) {
|
||||
throw new Error("You must specify a host to ping!");
|
||||
}
|
||||
|
||||
this._host = host;
|
||||
this._options = options = (options || {});
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
if (WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
} else if (LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/; // need to verify this
|
||||
} else if (MAC) {
|
||||
this._bin = "/sbin/ping";
|
||||
this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
} else {
|
||||
throw new Error("Could not detect your ping binary.");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this._bin)) {
|
||||
throw new Error("Could not detect " + this._bin + " on your system");
|
||||
}
|
||||
|
||||
this._i = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||
|
||||
// SEND A PING
|
||||
// ===========
|
||||
Ping.prototype.send = function(callback) {
|
||||
let self = this;
|
||||
callback = callback || function(err, ms) {
|
||||
if (err) {
|
||||
return self.emit("error", err);
|
||||
}
|
||||
return self.emit("result", ms);
|
||||
};
|
||||
|
||||
let _ended, _exited, _errored;
|
||||
|
||||
this._ping = spawn(this._bin, this._args); // spawn the binary
|
||||
|
||||
this._ping.on("error", function(err) { // handle binary errors
|
||||
_errored = true;
|
||||
callback(err);
|
||||
});
|
||||
|
||||
this._ping.stdout.on("data", function(data) { // log stdout
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
this._ping.stdout.on("end", function() {
|
||||
_ended = true;
|
||||
if (_exited && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
this._ping.stderr.on("data", function(data) { // log stderr
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
this._ping.on("exit", function(code) { // handle complete
|
||||
_exited = true;
|
||||
if (_ended && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
function onEnd() {
|
||||
let stdout = this.stdout._stdout,
|
||||
stderr = this.stderr._stderr,
|
||||
ms;
|
||||
|
||||
if (stderr) {
|
||||
return callback(new Error(stderr));
|
||||
}
|
||||
|
||||
if (!stdout) {
|
||||
return callback(new Error("No stdout detected"));
|
||||
}
|
||||
|
||||
ms = stdout.match(self._regmatch); // parse out the ##ms response
|
||||
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
|
||||
|
||||
callback(null, ms);
|
||||
}
|
||||
};
|
||||
|
||||
// CALL Ping#send(callback) ON A TIMER
|
||||
// ===================================
|
||||
Ping.prototype.start = function(callback) {
|
||||
let self = this;
|
||||
this._i = setInterval(function() {
|
||||
self.send(callback);
|
||||
}, (self._options.interval || 5000));
|
||||
self.send(callback);
|
||||
};
|
||||
|
||||
// STOP SENDING PINGS
|
||||
// ==================
|
||||
Ping.prototype.stop = function() {
|
||||
clearInterval(this._i);
|
||||
};
|
@@ -1,59 +0,0 @@
|
||||
const PrometheusClient = require('prom-client');
|
||||
|
||||
const commonLabels = [
|
||||
'monitor_name',
|
||||
'monitor_type',
|
||||
'monitor_url',
|
||||
'monitor_hostname',
|
||||
'monitor_port',
|
||||
]
|
||||
|
||||
const monitor_response_time = new PrometheusClient.Gauge({
|
||||
name: 'monitor_response_time',
|
||||
help: 'Monitor Response Time (ms)',
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
const monitor_status = new PrometheusClient.Gauge({
|
||||
name: 'monitor_status',
|
||||
help: 'Monitor Status (1 = UP, 0= DOWN)',
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
class Prometheus {
|
||||
monitorLabelValues = {}
|
||||
|
||||
constructor(monitor) {
|
||||
this.monitorLabelValues = {
|
||||
monitor_name: monitor.name,
|
||||
monitor_type: monitor.type,
|
||||
monitor_url: monitor.url,
|
||||
monitor_hostname: monitor.hostname,
|
||||
monitor_port: monitor.port
|
||||
}
|
||||
}
|
||||
|
||||
update(heartbeat) {
|
||||
try {
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === 'number') {
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
|
||||
} else {
|
||||
// Is it good?
|
||||
monitor_response_time.set(this.monitorLabelValues, -1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Prometheus
|
||||
}
|
788
server/server.js
788
server/server.js
@@ -1,788 +0,0 @@
|
||||
console.log("Welcome to Uptime Kuma")
|
||||
|
||||
const { sleep, debug } = require("../src/util");
|
||||
|
||||
console.log("Importing Node libraries")
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
|
||||
console.log("Importing 3rd-party libraries")
|
||||
debug("Importing express");
|
||||
const express = require("express");
|
||||
debug("Importing socket.io");
|
||||
const { Server } = require("socket.io");
|
||||
debug("Importing dayjs");
|
||||
const dayjs = require("dayjs");
|
||||
debug("Importing redbean-node");
|
||||
const { R } = require("redbean-node");
|
||||
debug("Importing jsonwebtoken");
|
||||
const jwt = require("jsonwebtoken");
|
||||
debug("Importing http-graceful-shutdown");
|
||||
const gracefulShutdown = require("http-graceful-shutdown");
|
||||
debug("Importing prometheus-api-metrics");
|
||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||
|
||||
console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting } = require("./util-server");
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const version = require("../package.json").version;
|
||||
const hostname = process.env.HOST || args.host || "0.0.0.0"
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
|
||||
console.info("Version: " + version)
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
app.use(express.json())
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
* @type {number}
|
||||
*/
|
||||
let totalClient = 0;
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
let monitorList = {};
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
*/
|
||||
let needSetup = false;
|
||||
|
||||
/**
|
||||
* Cache Index HTML
|
||||
* @type {string}
|
||||
*/
|
||||
let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
|
||||
(async () => {
|
||||
await initDatabase();
|
||||
|
||||
console.log("Adding route")
|
||||
|
||||
// Normal Router here
|
||||
|
||||
app.use("/", express.static("dist"));
|
||||
|
||||
// Basic Auth Router here
|
||||
|
||||
// Prometheus API metrics /metrics
|
||||
// With Basic Auth using the first user's username/password
|
||||
app.get("/metrics", basicAuth, prometheusAPIMetrics())
|
||||
|
||||
// Universal Route Handler, must be at the end
|
||||
app.get("*", function(request, response, next) {
|
||||
response.end(indexHTML)
|
||||
});
|
||||
|
||||
console.log("Adding socket handler")
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
})
|
||||
|
||||
totalClient++;
|
||||
|
||||
if (needSetup) {
|
||||
console.log("Redirect to setup page")
|
||||
socket.emit("setup")
|
||||
}
|
||||
|
||||
if (await setting("disableAuth")) {
|
||||
console.log("Disabled Auth: auto login to admin")
|
||||
await afterLogin(socket, await R.findOne("user", " username = 'admin' "))
|
||||
}
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
totalClient--;
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Public API
|
||||
// ***************************
|
||||
|
||||
socket.on("loginByToken", async (token, callback) => {
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
|
||||
console.log("Username from JWT: " + decoded.username)
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
decoded.username,
|
||||
])
|
||||
|
||||
if (user) {
|
||||
await afterLogin(socket, user)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
})
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "The user is inactive or deleted.",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid token.",
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.on("login", async (data, callback) => {
|
||||
console.log("Login")
|
||||
|
||||
let user = await login(data.username, data.password)
|
||||
|
||||
if (user) {
|
||||
await afterLogin(socket, user)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
})
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Incorrect username or password.",
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.on("logout", async (callback) => {
|
||||
socket.leave(socket.userID)
|
||||
socket.userID = null;
|
||||
callback();
|
||||
});
|
||||
|
||||
socket.on("needSetup", async (callback) => {
|
||||
callback(needSetup);
|
||||
});
|
||||
|
||||
socket.on("setup", async (username, password, callback) => {
|
||||
try {
|
||||
if ((await R.count("user")) !== 0) {
|
||||
throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database.")
|
||||
}
|
||||
|
||||
let user = R.dispense("user")
|
||||
user.username = username;
|
||||
user.password = passwordHash.generate(password)
|
||||
await R.store(user)
|
||||
|
||||
needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Auth Only API
|
||||
// ***************************
|
||||
|
||||
// Add a new monitor
|
||||
socket.on("add", async (monitor, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
let bean = R.dispense("monitor")
|
||||
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
delete monitor.notificationIDList;
|
||||
|
||||
bean.import(monitor)
|
||||
bean.user_id = socket.userID
|
||||
await R.store(bean)
|
||||
|
||||
await updateMonitorNotification(bean.id, notificationIDList)
|
||||
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
await sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Edit a monitor
|
||||
socket.on("editMonitor", async (monitor, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ])
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.")
|
||||
}
|
||||
|
||||
bean.name = monitor.name
|
||||
bean.type = monitor.type
|
||||
bean.url = monitor.url
|
||||
bean.interval = monitor.interval
|
||||
bean.hostname = monitor.hostname;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = monitor.port;
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
|
||||
await R.store(bean)
|
||||
|
||||
await updateMonitorNotification(bean.id, monitor.notificationIDList)
|
||||
|
||||
if (bean.active) {
|
||||
await restartMonitor(socket.userID, bean.id)
|
||||
}
|
||||
|
||||
await sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved.",
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitor", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`)
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
])
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
monitor: await bean.toJSON(),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start or Resume the monitor
|
||||
socket.on("resumeMonitor", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
await startMonitor(socket.userID, monitorID);
|
||||
await sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Resumed Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("pauseMonitor", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
await pauseMonitor(socket.userID, monitorID)
|
||||
await sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Paused Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteMonitor", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`)
|
||||
|
||||
if (monitorID in monitorList) {
|
||||
monitorList[monitorID].stop();
|
||||
delete monitorList[monitorID]
|
||||
}
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
});
|
||||
|
||||
await sendMonitorList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("changePassword", async (password, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
if (! password.currentPassword) {
|
||||
throw new Error("Invalid new password")
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID,
|
||||
])
|
||||
|
||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
||||
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(password.newPassword),
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
})
|
||||
} else {
|
||||
throw new Error("Incorrect current password")
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getSettings", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data: await getSettings("general"),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("setSettings", async (data, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await setSettings("general", data)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add or Edit
|
||||
socket.on("addNotification", async (notification, notificationID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await Notification.save(notification, notificationID, socket.userID)
|
||||
await sendNotificationList(socket)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteNotification", async (notificationID, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await Notification.delete(notificationID, socket.userID)
|
||||
await sendNotificationList(socket)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("testNotification", async (notification, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
let msg = await Notification.send(notification, notification.name + " Testing")
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
callback(Notification.checkApprise());
|
||||
} catch (e) {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("Init")
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
startMonitors();
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
for (let notificationID in notificationIDList) {
|
||||
if (notificationIDList[notificationID]) {
|
||||
let relation = R.dispense("monitor_notification");
|
||||
relation.monitor_id = monitorID;
|
||||
relation.notification_id = notificationID;
|
||||
await R.store(relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOwner(userID, monitorID) {
|
||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID,
|
||||
])
|
||||
|
||||
if (! row) {
|
||||
throw new Error("You do not own this monitor.");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMonitorList(socket) {
|
||||
let list = await getMonitorJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("monitorList", list)
|
||||
return list;
|
||||
}
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
let result = [];
|
||||
let list = await R.find("notification", " user_id = ? ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.export())
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
return list;
|
||||
}
|
||||
|
||||
async function afterLogin(socket, user) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id)
|
||||
|
||||
let monitorList = await sendMonitorList(socket)
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
sendHeartbeatList(socket, monitorID);
|
||||
sendImportantHeartbeatList(socket, monitorID);
|
||||
Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
|
||||
sendNotificationList(socket)
|
||||
|
||||
socket.emit("autoLogin")
|
||||
}
|
||||
|
||||
async function getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let monitorList = await R.find("monitor", " user_id = ? ", [
|
||||
userID,
|
||||
])
|
||||
|
||||
for (let monitor of monitorList) {
|
||||
result[monitor.id] = await monitor.toJSON();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkLogin(socket) {
|
||||
if (! socket.userID) {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database")
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connecting to Database")
|
||||
R.setup("sqlite", {
|
||||
filename: Database.path,
|
||||
});
|
||||
console.log("Connected")
|
||||
|
||||
// Patch the database
|
||||
await Database.patch()
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
||||
"jwtSecret",
|
||||
]);
|
||||
|
||||
if (! jwtSecretBean) {
|
||||
console.log("JWT secret is not found, generate one.")
|
||||
jwtSecretBean = R.dispense("setting")
|
||||
jwtSecretBean.key = "jwtSecret"
|
||||
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
|
||||
await R.store(jwtSecretBean)
|
||||
console.log("Stored JWT secret into database")
|
||||
} else {
|
||||
console.log("Load JWT secret from database.")
|
||||
}
|
||||
|
||||
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
|
||||
if ((await R.count("user")) === 0) {
|
||||
console.log("No user, need setup")
|
||||
needSetup = true;
|
||||
}
|
||||
|
||||
jwtSecret = jwtSecretBean.value;
|
||||
}
|
||||
|
||||
async function startMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID)
|
||||
|
||||
console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`)
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID,
|
||||
]);
|
||||
|
||||
let monitor = await R.findOne("monitor", " id = ? ", [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
if (monitor.id in monitorList) {
|
||||
monitorList[monitor.id].stop();
|
||||
}
|
||||
|
||||
monitorList[monitor.id] = monitor;
|
||||
monitor.start(io)
|
||||
}
|
||||
|
||||
async function restartMonitor(userID, monitorID) {
|
||||
return await startMonitor(userID, monitorID)
|
||||
}
|
||||
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID)
|
||||
|
||||
console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`)
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (monitorID in monitorList) {
|
||||
monitorList[monitorID].stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume active monitors
|
||||
*/
|
||||
async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ")
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io)
|
||||
monitorList[monitor.id] = monitor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID) {
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 100
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
let result = [];
|
||||
|
||||
for (let bean of list) {
|
||||
result.unshift(bean.toJSON())
|
||||
}
|
||||
|
||||
socket.emit("heartbeatList", monitorID, result)
|
||||
}
|
||||
|
||||
async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
AND important = 1
|
||||
ORDER BY time DESC
|
||||
LIMIT 500
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
socket.emit("importantHeartbeatList", monitorID, list)
|
||||
}
|
||||
|
||||
async function shutdownFunction(signal) {
|
||||
console.log("Shutdown requested");
|
||||
console.log("Called signal: " + signal);
|
||||
|
||||
console.log("Stopping all monitors")
|
||||
for (let id in monitorList) {
|
||||
let monitor = monitorList[id]
|
||||
monitor.stop()
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
console.log("Stopped DB")
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful Shutdown Done")
|
||||
}
|
||||
|
||||
gracefulShutdown(server, {
|
||||
signals: "SIGINT SIGTERM",
|
||||
timeout: 30000, // timeout: 30 secs
|
||||
development: false, // not in dev mode
|
||||
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
||||
finally: finalFunction, // finally function (sync) - e.g. for logging
|
||||
});
|
@@ -1,160 +0,0 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const { R } = require("redbean-node");
|
||||
const { debug } = require("../src/util");
|
||||
|
||||
exports.tcping = function (hostname, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tcpp.ping({
|
||||
address: hostname,
|
||||
port: port,
|
||||
attempts: 1,
|
||||
}, function(err, data) {
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (data.results.length >= 1 && data.results[0].err) {
|
||||
reject(data.results[0].err);
|
||||
}
|
||||
|
||||
resolve(Math.round(data.max));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.ping = function (hostname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname);
|
||||
|
||||
ping.send(function(err, ms) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else if (ms === null) {
|
||||
reject(new Error("timeout"))
|
||||
} else {
|
||||
resolve(Math.round(ms))
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.setting = async function (key) {
|
||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
key,
|
||||
]);
|
||||
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
debug(`Get Setting: ${key}: ${v}`)
|
||||
return v;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
])
|
||||
if (! bean) {
|
||||
bean = R.dispense("setting")
|
||||
bean.key = key;
|
||||
}
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean)
|
||||
}
|
||||
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
type,
|
||||
])
|
||||
|
||||
let result = {};
|
||||
|
||||
for (let row of list) {
|
||||
try {
|
||||
result[row.key] = JSON.parse(row.value);
|
||||
} catch (e) {
|
||||
result[row.key] = row.value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.setSettings = async function (type, data) {
|
||||
let keyList = Object.keys(data);
|
||||
|
||||
let promiseList = [];
|
||||
|
||||
for (let key of keyList) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key
|
||||
]);
|
||||
|
||||
if (bean == null) {
|
||||
bean = R.dispense("setting");
|
||||
bean.type = type;
|
||||
bean.key = key;
|
||||
}
|
||||
|
||||
if (bean.type === type) {
|
||||
bean.value = JSON.stringify(data[key]);
|
||||
promiseList.push(R.store(bean))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
// return an object containing the certificate information
|
||||
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
return -daysRemaining;
|
||||
}
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const {
|
||||
valid_from,
|
||||
valid_to,
|
||||
subjectaltname,
|
||||
issuer,
|
||||
fingerprint,
|
||||
} = res.request.res.socket.getPeerCertificate(false);
|
||||
|
||||
if (!valid_from || !valid_to || !subjectaltname) {
|
||||
throw {
|
||||
message: "No TLS certificate in response",
|
||||
};
|
||||
}
|
||||
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
const validTo = new Date(valid_to);
|
||||
|
||||
const validFor = subjectaltname
|
||||
.replace(/DNS:|IP Address:/g, "")
|
||||
.split(", ");
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
||||
|
||||
return {
|
||||
valid,
|
||||
validFor,
|
||||
validTo,
|
||||
daysRemaining,
|
||||
issuer,
|
||||
fingerprint,
|
||||
};
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
@@ -1,38 +0,0 @@
|
||||
@import "vars.scss";
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app {
|
||||
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
overflow: hidden;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
&.big-padding {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
|
||||
&:hover, &:active, &:focus, &.active {
|
||||
color: white;
|
||||
background-color: $highlight;
|
||||
border-color: $highlight;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
@@ -1,8 +0,0 @@
|
||||
$primary: #5CDD8B;
|
||||
$danger: #DC3545;
|
||||
$warning: #f8a306;
|
||||
$link-color: #111;
|
||||
$border-radius: 50rem;
|
||||
|
||||
$highlight: #7ce8a4;
|
||||
$highlight-white: #e7faec;
|
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div ref="modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
Confirm
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||
{{ yesText }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ noText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from "bootstrap"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
btnStyle: {
|
||||
type: String,
|
||||
default: "btn-primary",
|
||||
},
|
||||
yesText: {
|
||||
type: String,
|
||||
default: "Yes",
|
||||
},
|
||||
noText: {
|
||||
type: String,
|
||||
default: "No",
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
modal: null,
|
||||
}),
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal.show()
|
||||
},
|
||||
yes() {
|
||||
this.$emit("yes");
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<span v-if="isNum" ref="output">{{ output }}</span> <span v-if="isNum">{{ unit }}</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { sleep } from "../util.ts"
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
value: [String, Number],
|
||||
time: {
|
||||
type: Number,
|
||||
default: 0.3,
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
default: "ms",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
output: "",
|
||||
frameDuration: 30,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isNum() {
|
||||
return typeof this.value === "number"
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
async value(from, to) {
|
||||
let diff = to - from;
|
||||
let frames = 12;
|
||||
let step = Math.floor(diff / frames);
|
||||
|
||||
if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
|
||||
// Lazy to NOT this condition, hahaha.
|
||||
} else {
|
||||
for (let i = 1; i < frames; i++) {
|
||||
this.output += step;
|
||||
await sleep(15)
|
||||
}
|
||||
}
|
||||
|
||||
this.output = this.value;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.output = this.value;
|
||||
},
|
||||
|
||||
methods: {},
|
||||
|
||||
}
|
||||
</script>
|
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<span>{{ displayText }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
dateOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayText() {
|
||||
if (this.value !== undefined && this.value !== "") {
|
||||
let format = "YYYY-MM-DD HH:mm:ss";
|
||||
if (this.dateOnly) {
|
||||
format = "YYYY-MM-DD";
|
||||
}
|
||||
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div ref="wrap" class="wrap" :style="wrapStyle">
|
||||
<div class="hp-bar-big" :style="barStyle">
|
||||
<div
|
||||
v-for="(beat, index) in shortBeatList"
|
||||
:key="index"
|
||||
class="beat"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||
:style="beatStyle"
|
||||
:title="beat.msg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: "big",
|
||||
},
|
||||
monitorId: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
beatWidth: 10,
|
||||
beatHeight: 30,
|
||||
hoverScale: 1.5,
|
||||
beatMargin: 3, // Odd number only, even = blurry
|
||||
move: false,
|
||||
maxBeat: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
beatList() {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
return this.$root.heartbeatList[this.monitorId]
|
||||
},
|
||||
|
||||
shortBeatList() {
|
||||
let placeholders = []
|
||||
|
||||
let start = this.beatList.length - this.maxBeat;
|
||||
|
||||
if (this.move) {
|
||||
start = start - 1;
|
||||
}
|
||||
|
||||
if (start < 0) {
|
||||
// Add empty placeholder
|
||||
for (let i = start; i < 0; i++) {
|
||||
placeholders.push(0)
|
||||
}
|
||||
start = 0;
|
||||
}
|
||||
|
||||
return placeholders.concat(this.beatList.slice(start))
|
||||
},
|
||||
|
||||
wrapStyle() {
|
||||
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
|
||||
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
|
||||
|
||||
return {
|
||||
padding: `${topBottom}px ${leftRight}px`,
|
||||
width: "100%",
|
||||
}
|
||||
},
|
||||
|
||||
barStyle() {
|
||||
if (this.move && this.shortBeatList.length > this.maxBeat) {
|
||||
let width = -(this.beatWidth + this.beatMargin * 2);
|
||||
|
||||
return {
|
||||
transition: "all ease-in-out 0.25s",
|
||||
transform: `translateX(${width}px)`,
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
transform: "translateX(0)",
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
beatStyle() {
|
||||
return {
|
||||
width: this.beatWidth + "px",
|
||||
height: this.beatHeight + "px",
|
||||
margin: this.beatMargin + "px",
|
||||
"--hover-scale": this.hoverScale,
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
beatList: {
|
||||
handler(val, oldVal) {
|
||||
this.move = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.move = false;
|
||||
}, 300)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
mounted() {
|
||||
if (this.size === "small") {
|
||||
this.beatWidth = 5.6;
|
||||
this.beatMargin = 2.4;
|
||||
this.beatHeight = 16
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this.resize);
|
||||
this.resize();
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
if (this.$refs.wrap) {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.wrap {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hp-bar-big {
|
||||
.beat {
|
||||
display: inline-block;
|
||||
background-color: $primary;
|
||||
border-radius: 50rem;
|
||||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: $warning;
|
||||
}
|
||||
|
||||
&:not(.empty):hover {
|
||||
transition: all ease-in-out 0.15s;
|
||||
opacity: 0.8;
|
||||
transform: scale(var(--hover-scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<div class="form">
|
||||
<form @submit.prevent="submit">
|
||||
<h1 class="h3 mb-3 fw-normal" />
|
||||
|
||||
<div class="form-floating">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||
<label for="floatingInput">Username</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||
<label for="floatingPassword">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||
<div class="form-check">
|
||||
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||
|
||||
<label class="form-check-label" for="remember">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
|
||||
Login
|
||||
</button>
|
||||
|
||||
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
|
||||
{{ res.msg }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
username: "",
|
||||
password: "",
|
||||
|
||||
res: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.login(this.username, this.password, (res) => {
|
||||
this.processing = false;
|
||||
this.res = res;
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.form {
|
||||
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@@ -1,492 +0,0 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
Setup Notification
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Notification Type</label>
|
||||
<select id="type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">Email (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="pushover">Pushover</option>
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type === 'telegram'">
|
||||
<div class="mb-3">
|
||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
|
||||
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
|
||||
Auto Get
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<template v-if="notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'webhook'">
|
||||
<div class="mb-3">
|
||||
<label for="webhook-url" class="form-label">Post URL</label>
|
||||
<input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||
<select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
|
||||
<option value="json">
|
||||
application/json
|
||||
</option>
|
||||
<option value="form-data">
|
||||
multipart/form-data
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>"application/json" is good for any modern http servers such as express.js</p>
|
||||
<p>"multipart/form-data" is good for PHP, you just need to parse the json by <strong>json_decode($_POST['data'])</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'smtp'">
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="secure">
|
||||
Secure
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Generally, true for 465, false for other ports.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||
<input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'signal'">
|
||||
<div class="mb-3">
|
||||
<label for="signal-url" class="form-label">Post URL</label>
|
||||
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">Number</label>
|
||||
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
You need to have a signal client with REST API.
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can check this url to view how to setup one:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
IMPORTANT: You cannot mix groups and numbers in recipients!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'gotify'">
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">Priority</label>
|
||||
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
|
||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">Priority</label>
|
||||
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you want to send notifications to different devices, fill out Device field.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'apprise'">
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||
<p>
|
||||
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Status:
|
||||
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color:red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||
Test
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
|
||||
Are you sure want to delete this notification for all monitors?
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification"
|
||||
import Confirm from "./Confirm.vue";
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
notification: {
|
||||
name: "",
|
||||
type: null,
|
||||
gotifyPriority: 8,
|
||||
},
|
||||
appriseInstalled: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
telegramGetUpdatesURL() {
|
||||
let token = "<YOUR BOT TOKEN HERE>"
|
||||
|
||||
if (this.notification.telegramBotToken) {
|
||||
token = this.notification.telegramBotToken;
|
||||
}
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"notification.type"(to, from) {
|
||||
let oldName;
|
||||
|
||||
if (from) {
|
||||
oldName = `My ${ucfirst(from)} Alert (1)`;
|
||||
} else {
|
||||
oldName = "";
|
||||
}
|
||||
|
||||
if (! this.notification.name || this.notification.name === oldName) {
|
||||
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show()
|
||||
},
|
||||
|
||||
show(notificationID) {
|
||||
if (notificationID) {
|
||||
this.id = notificationID;
|
||||
|
||||
for (let n of this.$root.notificationList) {
|
||||
if (n.id === notificationID) {
|
||||
this.notification = JSON.parse(n.config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.id = null;
|
||||
this.notification = {
|
||||
name: "",
|
||||
type: null,
|
||||
}
|
||||
|
||||
// Default set to Telegram
|
||||
this.notification.type = "telegram"
|
||||
this.notification.gotifyPriority = 8
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.processing = false;
|
||||
})
|
||||
},
|
||||
|
||||
deleteNotification() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async autoGetTelegramChatID() {
|
||||
try {
|
||||
let res = await axios.get(this.telegramGetUpdatesURL)
|
||||
|
||||
if (res.data.result.length >= 1) {
|
||||
let update = res.data.result[res.data.result.length - 1]
|
||||
|
||||
if (update.channel_post) {
|
||||
this.notification.telegramChatID = update.channel_post.chat.id;
|
||||
} else if (update.message) {
|
||||
this.notification.telegramChatID = update.message.chat.id;
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + color ">{{ text }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
status: Number,
|
||||
},
|
||||
|
||||
computed: {
|
||||
color() {
|
||||
if (this.status === 0) {
|
||||
return "danger"
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "primary"
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
},
|
||||
|
||||
text() {
|
||||
if (this.status === 0) {
|
||||
return "Down"
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "Up"
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "Pending"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
width: 64px;
|
||||
}
|
||||
</style>
|
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<span :class="className">{{ uptime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
monitor: Object,
|
||||
type: String,
|
||||
pill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
uptime() {
|
||||
|
||||
let key = this.monitor.id + "_" + this.type;
|
||||
|
||||
if (this.$root.uptimeList[key] !== undefined) {
|
||||
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
||||
}
|
||||
|
||||
return "N/A"
|
||||
},
|
||||
|
||||
color() {
|
||||
if (this.lastHeartBeat.status === 0) {
|
||||
return "danger"
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 1) {
|
||||
return "primary"
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 2) {
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
},
|
||||
|
||||
lastHeartBeat() {
|
||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
||||
}
|
||||
|
||||
return {
|
||||
status: -1,
|
||||
}
|
||||
},
|
||||
|
||||
className() {
|
||||
if (this.pill) {
|
||||
return `badge rounded-pill bg-${this.color}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
min-width: 62px;
|
||||
}
|
||||
</style>
|
10
src/icon.js
10
src/icon.js
@@ -1,10 +0,0 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
|
||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||
|
||||
// Add Free Font Awesome Icons here
|
||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
|
||||
|
||||
export { FontAwesomeIcon }
|
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||
<div class="container-fluid">
|
||||
{{ $root.connectionErrorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<font-awesome-icon icon="tachometer-alt" /> Dashboard
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<font-awesome-icon icon="cog" /> Settings
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<!-- Mobile header -->
|
||||
<header v-else class="d-flex flex-wrap justify-content-center mt-3 mb-3">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Add :key to disable vue router re-use the same component -->
|
||||
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
Uptime Kuma -
|
||||
Version: {{ $root.info.version }} -
|
||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Mobile Only -->
|
||||
<div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
|
||||
<nav v-if="$root.isMobile" class="bottom-nav">
|
||||
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
|
||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||
Dashboard
|
||||
</router-link>
|
||||
|
||||
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile">
|
||||
<div><font-awesome-icon icon="list" /></div>
|
||||
List
|
||||
</a>
|
||||
|
||||
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
|
||||
<div><font-awesome-icon icon="plus" /></div>
|
||||
Add
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
|
||||
<div><font-awesome-icon icon="cog" /></div>
|
||||
Settings
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Login from "../components/Login.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Login,
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (this.$route.name === "root") {
|
||||
this.$router.push("/dashboard")
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.bottom-nav {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 10px;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
width: 25%;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
padding: 8px 10px 0;
|
||||
font-size: 13px;
|
||||
color: #c1c1c1;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: $primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.lost-connection {
|
||||
padding: 5px;
|
||||
background-color: crimson;
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #AAA;
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
margin-left: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
93
src/main.js
93
src/main.js
@@ -1,93 +0,0 @@
|
||||
import "bootstrap";
|
||||
import { createApp, h } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
import App from "./App.vue";
|
||||
import "./assets/app.scss";
|
||||
import { FontAwesomeIcon } from "./icon.js";
|
||||
import EmptyLayout from "./layouts/EmptyLayout.vue";
|
||||
import Layout from "./layouts/Layout.vue";
|
||||
import socket from "./mixins/socket";
|
||||
import Dashboard from "./pages/Dashboard.vue";
|
||||
import DashboardHome from "./pages/DashboardHome.vue";
|
||||
import Details from "./pages/Details.vue";
|
||||
import EditMonitor from "./pages/EditMonitor.vue";
|
||||
import Settings from "./pages/Settings.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
name: "root",
|
||||
path: "",
|
||||
component: Dashboard,
|
||||
children: [
|
||||
{
|
||||
name: "DashboardHome",
|
||||
path: "/dashboard",
|
||||
component: DashboardHome,
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:id",
|
||||
component: EmptyLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: Details,
|
||||
},
|
||||
{
|
||||
path: "/edit/:id",
|
||||
component: EditMonitor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/add",
|
||||
component: EditMonitor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
path: "/setup",
|
||||
component: Setup,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
linkActiveClass: "active",
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
const app = createApp({
|
||||
mixins: [
|
||||
socket,
|
||||
],
|
||||
render: () => h(App),
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
|
||||
const options = {
|
||||
position: "bottom-right",
|
||||
};
|
||||
|
||||
app.use(Toast, options);
|
||||
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon)
|
||||
|
||||
app.mount("#app")
|
@@ -1,349 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { io } from "socket.io-client";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast()
|
||||
|
||||
let socket;
|
||||
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
info: { },
|
||||
socket: {
|
||||
token: null,
|
||||
firstConnect: true,
|
||||
connected: false,
|
||||
connectCount: 0,
|
||||
},
|
||||
remember: (localStorage.remember !== "0"),
|
||||
userTimezone: localStorage.timezone || "auto",
|
||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||
loggedIn: false,
|
||||
monitorList: { },
|
||||
heartbeatList: { },
|
||||
importantHeartbeatList: { },
|
||||
avgPingList: { },
|
||||
uptimeList: { },
|
||||
certInfoList: {},
|
||||
notificationList: [],
|
||||
windowWidth: window.innerWidth,
|
||||
showListMobile: false,
|
||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
let wsHost;
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
wsHost = ":3001"
|
||||
} else {
|
||||
wsHost = ""
|
||||
}
|
||||
|
||||
socket = io(wsHost, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on("info", (info) => {
|
||||
this.info = info;
|
||||
});
|
||||
|
||||
socket.on("setup", (monitorID, data) => {
|
||||
this.$router.push("/setup")
|
||||
});
|
||||
|
||||
socket.on("autoLogin", (monitorID, data) => {
|
||||
this.loggedIn = true;
|
||||
this.storage().token = "autoLogin";
|
||||
this.allowLoginDialog = false;
|
||||
});
|
||||
|
||||
socket.on("monitorList", (data) => {
|
||||
// Add Helper function
|
||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||
monitor.getUrl = () => {
|
||||
try {
|
||||
return new URL(monitor.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
this.monitorList = data;
|
||||
});
|
||||
|
||||
socket.on("notificationList", (data) => {
|
||||
this.notificationList = data;
|
||||
});
|
||||
|
||||
socket.on("heartbeat", (data) => {
|
||||
if (! (data.monitorID in this.heartbeatList)) {
|
||||
this.heartbeatList[data.monitorID] = [];
|
||||
}
|
||||
|
||||
this.heartbeatList[data.monitorID].push(data)
|
||||
|
||||
// Add to important list if it is important
|
||||
// Also toast
|
||||
if (data.important) {
|
||||
|
||||
if (data.status === 0) {
|
||||
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
|
||||
timeout: false,
|
||||
});
|
||||
} else if (data.status === 1) {
|
||||
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
|
||||
timeout: 20000,
|
||||
});
|
||||
} else {
|
||||
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
||||
}
|
||||
|
||||
if (! (data.monitorID in this.importantHeartbeatList)) {
|
||||
this.importantHeartbeatList[data.monitorID] = [];
|
||||
}
|
||||
|
||||
this.importantHeartbeatList[data.monitorID].unshift(data)
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("heartbeatList", (monitorID, data) => {
|
||||
if (! (monitorID in this.heartbeatList)) {
|
||||
this.heartbeatList[monitorID] = data;
|
||||
} else {
|
||||
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("avgPing", (monitorID, data) => {
|
||||
this.avgPingList[monitorID] = data
|
||||
});
|
||||
|
||||
socket.on("uptime", (monitorID, type, data) => {
|
||||
this.uptimeList[`${monitorID}_${type}`] = data
|
||||
});
|
||||
|
||||
socket.on("certInfo", (monitorID, data) => {
|
||||
this.certInfoList[monitorID] = JSON.parse(data)
|
||||
});
|
||||
|
||||
socket.on("importantHeartbeatList", (monitorID, data) => {
|
||||
if (! (monitorID in this.importantHeartbeatList)) {
|
||||
this.importantHeartbeatList[monitorID] = data;
|
||||
} else {
|
||||
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||
this.socket.connected = false;
|
||||
this.socket.firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect")
|
||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
this.socket.connected = false;
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("connect")
|
||||
this.socket.connectCount++;
|
||||
this.socket.connected = true;
|
||||
|
||||
// Reset Heartbeat list if it is re-connect
|
||||
if (this.socket.connectCount >= 2) {
|
||||
this.clearData()
|
||||
}
|
||||
|
||||
let token = this.storage().token;
|
||||
|
||||
if (token) {
|
||||
if (token !== "autoLogin") {
|
||||
this.loginByToken(token)
|
||||
} else {
|
||||
|
||||
// Timeout if it is not actually auto login
|
||||
setTimeout(() => {
|
||||
if (! this.loggedIn) {
|
||||
this.allowLoginDialog = true;
|
||||
this.$root.storage().removeItem("token");
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
} else {
|
||||
this.allowLoginDialog = true;
|
||||
}
|
||||
|
||||
this.socket.firstConnect = false;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
cancelActiveList() {
|
||||
this.$root.showListMobile = false;
|
||||
},
|
||||
|
||||
onResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
|
||||
storage() {
|
||||
return (this.remember) ? localStorage : sessionStorage;
|
||||
},
|
||||
|
||||
getSocket() {
|
||||
return socket;
|
||||
},
|
||||
|
||||
toastRes(res) {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
},
|
||||
|
||||
login(username, password, callback) {
|
||||
socket.emit("login", {
|
||||
username,
|
||||
password,
|
||||
}, (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
this.storage().token = res.token;
|
||||
this.socket.token = res.token;
|
||||
this.loggedIn = true;
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "")
|
||||
}
|
||||
|
||||
callback(res)
|
||||
})
|
||||
},
|
||||
|
||||
loginByToken(token) {
|
||||
socket.emit("loginByToken", token, (res) => {
|
||||
this.allowLoginDialog = true;
|
||||
|
||||
if (! res.ok) {
|
||||
this.logout()
|
||||
} else {
|
||||
this.loggedIn = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.storage().removeItem("token");
|
||||
this.socket.token = null;
|
||||
this.loggedIn = false;
|
||||
|
||||
this.clearData()
|
||||
},
|
||||
|
||||
add(monitor, callback) {
|
||||
socket.emit("add", monitor, callback)
|
||||
},
|
||||
|
||||
deleteMonitor(monitorID, callback) {
|
||||
socket.emit("deleteMonitor", monitorID, callback)
|
||||
},
|
||||
|
||||
clearData() {
|
||||
console.log("reset heartbeat list")
|
||||
this.heartbeatList = {}
|
||||
this.importantHeartbeatList = {}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
isMobile() {
|
||||
return this.windowWidth <= 767.98;
|
||||
},
|
||||
|
||||
timezone() {
|
||||
|
||||
if (this.userTimezone === "auto") {
|
||||
return dayjs.tz.guess()
|
||||
}
|
||||
|
||||
return this.userTimezone
|
||||
},
|
||||
|
||||
lastHeartbeatList() {
|
||||
let result = {}
|
||||
|
||||
for (let monitorID in this.heartbeatList) {
|
||||
let index = this.heartbeatList[monitorID].length - 1;
|
||||
result[monitorID] = this.heartbeatList[monitorID][index];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
statusList() {
|
||||
let result = {}
|
||||
|
||||
let unknown = {
|
||||
text: "Unknown",
|
||||
color: "secondary",
|
||||
}
|
||||
|
||||
for (let monitorID in this.lastHeartbeatList) {
|
||||
let lastHeartBeat = this.lastHeartbeatList[monitorID]
|
||||
|
||||
if (! lastHeartBeat) {
|
||||
result[monitorID] = unknown;
|
||||
} else if (lastHeartBeat.status === 1) {
|
||||
result[monitorID] = {
|
||||
text: "Up",
|
||||
color: "primary",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 0) {
|
||||
result[monitorID] = {
|
||||
text: "Down",
|
||||
color: "danger",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 2) {
|
||||
result[monitorID] = {
|
||||
text: "Pending",
|
||||
color: "warning",
|
||||
};
|
||||
} else {
|
||||
result[monitorID] = unknown;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
// Reload the SPA if the server version is changed.
|
||||
"info.version"(to, from) {
|
||||
if (from && from !== to) {
|
||||
window.location.reload()
|
||||
}
|
||||
},
|
||||
|
||||
remember() {
|
||||
localStorage.remember = (this.remember) ? "1" : "0"
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
}
|
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-5 col-xl-4">
|
||||
<div v-if="! $root.isMobile">
|
||||
<router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="showList" class="shadow-box list mb-4">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" @click="$root.cancelActiveList">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-7 col-xl-8">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
|
||||
return result;
|
||||
},
|
||||
showList() {
|
||||
return ! this.$root.isMobile || this.$root.showListMobile;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.container-fluid {
|
||||
width: 98%
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 25px;
|
||||
height: auto;
|
||||
min-height: calc(100vh - 200px);
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 15px 15px 12px 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
</style>
|
@@ -1,193 +0,0 @@
|
||||
<template>
|
||||
<div v-if="$route.name === 'DashboardHome'">
|
||||
<h1 class="mb-3">
|
||||
Quick Stats
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Up</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Down</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Unknown</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Pause</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="false" class="row">
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(30-day)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box" style="margin-top: 25px;">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<td>{{ beat.name }}</td>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
<td>{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="4">
|
||||
No important events
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-center kuma_pagination">
|
||||
<pagination
|
||||
v-model="page"
|
||||
:records="importantHeartBeatList.length"
|
||||
:per-page="perPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view ref="child" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Status from "../components/Status.vue";
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Datetime,
|
||||
Status,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
heartBeatList: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stats() {
|
||||
let result = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
|
||||
for (let monitorID in this.$root.monitorList) {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID]
|
||||
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
result.up++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
importantHeartBeatList() {
|
||||
let result = [];
|
||||
|
||||
for (let monitorID in this.$root.importantHeartbeatList) {
|
||||
let list = this.$root.importantHeartbeatList[monitorID]
|
||||
result = result.concat(list);
|
||||
}
|
||||
|
||||
for (let beat of result) {
|
||||
let monitor = this.$root.monitorList[beat.monitorID];
|
||||
|
||||
if (monitor) {
|
||||
beat.name = monitor.name
|
||||
}
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
if (a.time > b.time) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.time < b.time) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
this.heartBeatList = result;
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
displayedRecords() {
|
||||
const startIndex = this.perPage * (this.page - 1);
|
||||
const endIndex = startIndex + this.perPage;
|
||||
return this.heartBeatList.slice(startIndex, endIndex);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars";
|
||||
|
||||
.num {
|
||||
font-size: 30px;
|
||||
color: $primary;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
|
||||
tr {
|
||||
transition: all ease-in-out 0.2ms;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,355 +0,0 @@
|
||||
<template>
|
||||
<h1> {{ monitor.name }}</h1>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br>
|
||||
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||
<font-awesome-icon icon="pause" /> Pause
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> Resume
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> Edit
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<HeartbeatBar :monitor-id="monitor.id" />
|
||||
<span class="word">Check every {{ monitor.interval }} seconds.</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box big-padding text-center stats">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>{{ pingTitle }}</h4>
|
||||
<p>(Current)</p>
|
||||
<span class="num"><CountUp :value="ping" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Avg. {{ pingTitle }}</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><CountUp :value="avgPing" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(30-day)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||
</div>
|
||||
|
||||
<div v-if="certInfo" class="col">
|
||||
<h4>Cert Exp.</h4>
|
||||
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Certificate Info</h4>
|
||||
<table class="text-start">
|
||||
<tbody>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Valid:
|
||||
</td>
|
||||
<td>{{ certInfo.valid }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Valid To:
|
||||
</td>
|
||||
<td><Datetime :value="certInfo.validTo" /></td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Days Remaining:
|
||||
</td>
|
||||
<td>{{ certInfo.daysRemaining }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Issuer:
|
||||
</td>
|
||||
<td>{{ certInfo.issuer }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Fingerprint:
|
||||
</td>
|
||||
<td>{{ certInfo.fingerprint }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
<td>{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="3">
|
||||
No important events
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-center kuma_pagination">
|
||||
<pagination
|
||||
v-model="page"
|
||||
:records="importantHeartBeatList.length"
|
||||
:per-page="perPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" @yes="pauseMonitor">
|
||||
Are you sure want to pause?
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
|
||||
Are you sure want to delete this monitor?
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Status from "../components/Status.vue";
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
import CountUp from "../components/CountUp.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
CountUp,
|
||||
Datetime,
|
||||
HeartbeatBar,
|
||||
Confirm,
|
||||
Status,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
heartBeatList: [],
|
||||
toggleCertInfoBox: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
pingTitle() {
|
||||
if (this.monitor.type === "http") {
|
||||
return "Response"
|
||||
}
|
||||
|
||||
return "Ping"
|
||||
},
|
||||
|
||||
monitor() {
|
||||
let id = this.$route.params.id
|
||||
return this.$root.monitorList[id];
|
||||
},
|
||||
|
||||
lastHeartBeat() {
|
||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
||||
}
|
||||
|
||||
return {
|
||||
status: -1,
|
||||
}
|
||||
},
|
||||
|
||||
ping() {
|
||||
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
|
||||
return this.lastHeartBeat.ping;
|
||||
}
|
||||
|
||||
return "N/A"
|
||||
},
|
||||
|
||||
avgPing() {
|
||||
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
|
||||
return this.$root.avgPingList[this.monitor.id];
|
||||
}
|
||||
|
||||
return "N/A"
|
||||
},
|
||||
|
||||
importantHeartBeatList() {
|
||||
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
||||
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
|
||||
return this.$root.importantHeartbeatList[this.monitor.id]
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
status() {
|
||||
if (this.$root.statusList[this.monitor.id]) {
|
||||
return this.$root.statusList[this.monitor.id]
|
||||
}
|
||||
|
||||
return { }
|
||||
},
|
||||
|
||||
certInfo() {
|
||||
if (this.$root.certInfoList[this.monitor.id]) {
|
||||
return this.$root.certInfoList[this.monitor.id]
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
showCertInfoBox() {
|
||||
return this.certInfo != null && this.toggleCertInfoBox;
|
||||
},
|
||||
|
||||
displayedRecords() {
|
||||
const startIndex = this.perPage * (this.page - 1);
|
||||
const endIndex = startIndex + this.perPage;
|
||||
return this.heartBeatList.slice(startIndex, endIndex);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
testNotification() {
|
||||
this.$root.getSocket().emit("testNotification", this.monitor.id)
|
||||
toast.success("Test notification is requested.")
|
||||
},
|
||||
|
||||
pauseDialog() {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
|
||||
resumeMonitor() {
|
||||
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
})
|
||||
},
|
||||
|
||||
pauseMonitor() {
|
||||
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
})
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
deleteMonitor() {
|
||||
this.$root.deleteMonitor(this.monitor.id, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/dashboard")
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.url {
|
||||
color: $primary;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.functions {
|
||||
button, a {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.word {
|
||||
color: #AAA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
|
||||
tr {
|
||||
transition: all ease-in-out 0.2ms;
|
||||
}
|
||||
}
|
||||
|
||||
.stats p {
|
||||
font-size: 13px;
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
.stats {
|
||||
padding: 10px;
|
||||
|
||||
.col {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,216 +0,0 @@
|
||||
<template>
|
||||
<h1 class="mb-3">
|
||||
{{ pageName }}
|
||||
</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>General</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Monitor Type</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
</option>
|
||||
<option value="port">
|
||||
TCP Port
|
||||
</option>
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - Keyword
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'keyword' " class="mb-3">
|
||||
<label for="keyword" class="form-label">Keyword</label>
|
||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
Search keyword in plain html or JSON response and it is case-sensitive
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' " class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="maxRetries" class="form-label">Retries</label>
|
||||
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum retries before the service is marked as down and a notification is sent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
Ignore TLS/SSL error for HTTPS websites
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
Upside Down Mode
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
|
||||
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3">
|
||||
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
|
||||
|
||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||
{{ notification.name }}
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
monitor: {
|
||||
notificationIDList: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageName() {
|
||||
return (this.isAdd) ? "Add New Monitor" : "Edit"
|
||||
},
|
||||
isAdd() {
|
||||
return this.$route.path === "/add";
|
||||
},
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/edit");
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$route.fullPath" () {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (this.isAdd) {
|
||||
console.log("??????")
|
||||
this.monitor = {
|
||||
type: "http",
|
||||
name: "",
|
||||
url: "https://",
|
||||
interval: 60,
|
||||
maxretries: 0,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
}
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.monitor = res.monitor;
|
||||
} else {
|
||||
toast.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.add(this.monitor, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/dashboard/" + res.monitorID)
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
@@ -1,208 +0,0 @@
|
||||
<template>
|
||||
<h1 class="mb-3">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>General</h2>
|
||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">Timezone</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
Auto: {{ guessTimezone }}
|
||||
</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template v-if="loaded">
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2>Change Password</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
<p v-else>
|
||||
Please assign a notification to monitor(s) to get it to work.
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
import { timezoneList } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationDialog,
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timezoneList: timezoneList(),
|
||||
guessTimezone: dayjs.tz.guess(),
|
||||
|
||||
invalidPassword: false,
|
||||
password: {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
repeatNewPassword: "",
|
||||
},
|
||||
settings: {
|
||||
|
||||
},
|
||||
loaded: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"password.repeatNewPassword"() {
|
||||
this.invalidPassword = false;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
saveGeneral() {
|
||||
localStorage.timezone = this.$root.userTimezone;
|
||||
toast.success("Saved.")
|
||||
},
|
||||
|
||||
savePassword() {
|
||||
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||
this.invalidPassword = true;
|
||||
} else {
|
||||
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
if (res.ok) {
|
||||
this.password.currentPassword = ""
|
||||
this.password.newPassword = ""
|
||||
this.password.repeatNewPassword = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
this.$root.getSocket().emit("getSettings", (res) => {
|
||||
this.settings = res.data;
|
||||
this.loaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.loadSettings();
|
||||
})
|
||||
},
|
||||
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
||||
disableAuth() {
|
||||
this.settings.disableAuth = true;
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
this.$root.storage().removeItem("token");
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<div class="form">
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<object width="64" height="64" data="/icon.svg" />
|
||||
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
|
||||
Uptime Kuma
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-3">
|
||||
Create your admin account
|
||||
</p>
|
||||
|
||||
<div class="form-floating">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
|
||||
<label for="floatingInput">Username</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
|
||||
<label for="floatingPassword">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
|
||||
<label for="repeat">Repeat Password</label>
|
||||
</div>
|
||||
|
||||
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
username: "",
|
||||
password: "",
|
||||
repeatPassword: "",
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getSocket().emit("needSetup", (needSetup) => {
|
||||
if (! needSetup) {
|
||||
this.$router.push("/")
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.password !== this.repeatPassword) {
|
||||
toast.error("Repeat password do not match.")
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("setup", this.username, this.password, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res)
|
||||
|
||||
if (res.ok) {
|
||||
this.$router.push("/")
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.form {
|
||||
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@@ -1,404 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
function getTimezoneOffset(timeZone) {
|
||||
const now = new Date();
|
||||
const tzString = now.toLocaleString("en-US", {
|
||||
timeZone,
|
||||
});
|
||||
const localString = now.toLocaleString("en-US");
|
||||
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
|
||||
const offset = diff + now.getTimezoneOffset() / 60;
|
||||
return -offset;
|
||||
}
|
||||
|
||||
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
||||
// TODO: Move to separate file
|
||||
const aryIannaTimeZones = [
|
||||
"Europe/Andorra",
|
||||
"Asia/Dubai",
|
||||
"Asia/Kabul",
|
||||
"Europe/Tirane",
|
||||
"Asia/Yerevan",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Cordoba",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/Jujuy",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/Mendoza",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Ushuaia",
|
||||
"Pacific/Pago_Pago",
|
||||
"Europe/Vienna",
|
||||
"Australia/Lord_Howe",
|
||||
"Antarctica/Macquarie",
|
||||
"Australia/Hobart",
|
||||
"Australia/Currie",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Sydney",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Darwin",
|
||||
"Australia/Perth",
|
||||
"Australia/Eucla",
|
||||
"Asia/Baku",
|
||||
"America/Barbados",
|
||||
"Asia/Dhaka",
|
||||
"Europe/Brussels",
|
||||
"Europe/Sofia",
|
||||
"Atlantic/Bermuda",
|
||||
"Asia/Brunei",
|
||||
"America/La_Paz",
|
||||
"America/Noronha",
|
||||
"America/Belem",
|
||||
"America/Fortaleza",
|
||||
"America/Recife",
|
||||
"America/Araguaina",
|
||||
"America/Maceio",
|
||||
"America/Bahia",
|
||||
"America/Sao_Paulo",
|
||||
"America/Campo_Grande",
|
||||
"America/Cuiaba",
|
||||
"America/Santarem",
|
||||
"America/Porto_Velho",
|
||||
"America/Boa_Vista",
|
||||
"America/Manaus",
|
||||
"America/Eirunepe",
|
||||
"America/Rio_Branco",
|
||||
"America/Nassau",
|
||||
"Asia/Thimphu",
|
||||
"Europe/Minsk",
|
||||
"America/Belize",
|
||||
"America/St_Johns",
|
||||
"America/Halifax",
|
||||
"America/Glace_Bay",
|
||||
"America/Moncton",
|
||||
"America/Goose_Bay",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Toronto",
|
||||
"America/Nipigon",
|
||||
"America/Thunder_Bay",
|
||||
"America/Iqaluit",
|
||||
"America/Pangnirtung",
|
||||
"America/Atikokan",
|
||||
"America/Winnipeg",
|
||||
"America/Rainy_River",
|
||||
"America/Resolute",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Regina",
|
||||
"America/Swift_Current",
|
||||
"America/Edmonton",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Yellowknife",
|
||||
"America/Inuvik",
|
||||
"America/Creston",
|
||||
"America/Dawson_Creek",
|
||||
"America/Fort_Nelson",
|
||||
"America/Vancouver",
|
||||
"America/Whitehorse",
|
||||
"America/Dawson",
|
||||
"Indian/Cocos",
|
||||
"Europe/Zurich",
|
||||
"Africa/Abidjan",
|
||||
"Pacific/Rarotonga",
|
||||
"America/Santiago",
|
||||
"America/Punta_Arenas",
|
||||
"Pacific/Easter",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Urumqi",
|
||||
"America/Bogota",
|
||||
"America/Costa_Rica",
|
||||
"America/Havana",
|
||||
"Atlantic/Cape_Verde",
|
||||
"America/Curacao",
|
||||
"Indian/Christmas",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Famagusta",
|
||||
"Europe/Prague",
|
||||
"Europe/Berlin",
|
||||
"Europe/Copenhagen",
|
||||
"America/Santo_Domingo",
|
||||
"Africa/Algiers",
|
||||
"America/Guayaquil",
|
||||
"Pacific/Galapagos",
|
||||
"Europe/Tallinn",
|
||||
"Africa/Cairo",
|
||||
"Africa/El_Aaiun",
|
||||
"Europe/Madrid",
|
||||
"Africa/Ceuta",
|
||||
"Atlantic/Canary",
|
||||
"Europe/Helsinki",
|
||||
"Pacific/Fiji",
|
||||
"Atlantic/Stanley",
|
||||
"Pacific/Chuuk",
|
||||
"Pacific/Pohnpei",
|
||||
"Pacific/Kosrae",
|
||||
"Atlantic/Faroe",
|
||||
"Europe/Paris",
|
||||
"Europe/London",
|
||||
"Asia/Tbilisi",
|
||||
"America/Cayenne",
|
||||
"Africa/Accra",
|
||||
"Europe/Gibraltar",
|
||||
"America/Godthab",
|
||||
"America/Danmarkshavn",
|
||||
"America/Scoresbysund",
|
||||
"America/Thule",
|
||||
"Europe/Athens",
|
||||
"Atlantic/South_Georgia",
|
||||
"America/Guatemala",
|
||||
"Pacific/Guam",
|
||||
"Africa/Bissau",
|
||||
"America/Guyana",
|
||||
"Asia/Hong_Kong",
|
||||
"America/Tegucigalpa",
|
||||
"America/Port-au-Prince",
|
||||
"Europe/Budapest",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Makassar",
|
||||
"Asia/Jayapura",
|
||||
"Europe/Dublin",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kolkata",
|
||||
"Indian/Chagos",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Tehran",
|
||||
"Atlantic/Reykjavik",
|
||||
"Europe/Rome",
|
||||
"America/Jamaica",
|
||||
"Asia/Amman",
|
||||
"Asia/Tokyo",
|
||||
"Africa/Nairobi",
|
||||
"Asia/Bishkek",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Kiritimati",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Seoul",
|
||||
"Asia/Almaty",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Oral",
|
||||
"Asia/Beirut",
|
||||
"Asia/Colombo",
|
||||
"Africa/Monrovia",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Riga",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Casablanca",
|
||||
"Europe/Monaco",
|
||||
"Europe/Chisinau",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Kwajalein",
|
||||
"Asia/Yangon",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Hovd",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Macau",
|
||||
"America/Martinique",
|
||||
"Europe/Malta",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Maldives",
|
||||
"America/Mexico_City",
|
||||
"America/Cancun",
|
||||
"America/Merida",
|
||||
"America/Monterrey",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Chihuahua",
|
||||
"America/Ojinaga",
|
||||
"America/Hermosillo",
|
||||
"America/Tijuana",
|
||||
"America/Bahia_Banderas",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Africa/Maputo",
|
||||
"Africa/Windhoek",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Norfolk",
|
||||
"Africa/Lagos",
|
||||
"America/Managua",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Oslo",
|
||||
"Asia/Kathmandu",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Chatham",
|
||||
"America/Panama",
|
||||
"America/Lima",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Bougainville",
|
||||
"Asia/Manila",
|
||||
"Asia/Karachi",
|
||||
"Europe/Warsaw",
|
||||
"America/Miquelon",
|
||||
"Pacific/Pitcairn",
|
||||
"America/Puerto_Rico",
|
||||
"Asia/Gaza",
|
||||
"Asia/Hebron",
|
||||
"Europe/Lisbon",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Azores",
|
||||
"Pacific/Palau",
|
||||
"America/Asuncion",
|
||||
"Asia/Qatar",
|
||||
"Indian/Reunion",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Moscow",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Kirov",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Saratov",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Samara",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Omsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Chita",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Magadan",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Riyadh",
|
||||
"Pacific/Guadalcanal",
|
||||
"Indian/Mahe",
|
||||
"Africa/Khartoum",
|
||||
"Europe/Stockholm",
|
||||
"Asia/Singapore",
|
||||
"America/Paramaribo",
|
||||
"Africa/Juba",
|
||||
"Africa/Sao_Tome",
|
||||
"America/El_Salvador",
|
||||
"Asia/Damascus",
|
||||
"America/Grand_Turk",
|
||||
"Africa/Ndjamena",
|
||||
"Indian/Kerguelen",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Dushanbe",
|
||||
"Pacific/Fakaofo",
|
||||
"Asia/Dili",
|
||||
"Asia/Ashgabat",
|
||||
"Africa/Tunis",
|
||||
"Pacific/Tongatapu",
|
||||
"Europe/Istanbul",
|
||||
"America/Port_of_Spain",
|
||||
"Pacific/Funafuti",
|
||||
"Asia/Taipei",
|
||||
"Europe/Kiev",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Zaporozhye",
|
||||
"Pacific/Wake",
|
||||
"America/New_York",
|
||||
"America/Detroit",
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Chicago",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Knox",
|
||||
"America/Menominee",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/Denver",
|
||||
"America/Boise",
|
||||
"America/Phoenix",
|
||||
"America/Los_Angeles",
|
||||
"America/Anchorage",
|
||||
"America/Juneau",
|
||||
"America/Sitka",
|
||||
"America/Metlakatla",
|
||||
"America/Yakutat",
|
||||
"America/Nome",
|
||||
"America/Adak",
|
||||
"Pacific/Honolulu",
|
||||
"America/Montevideo",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Tashkent",
|
||||
"America/Caracas",
|
||||
"Asia/Ho_Chi_Minh",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Wallis",
|
||||
"Pacific/Apia",
|
||||
"Africa/Johannesburg",
|
||||
];
|
||||
|
||||
export function timezoneList() {
|
||||
|
||||
let result = [];
|
||||
|
||||
for (let timezone of aryIannaTimeZones) {
|
||||
|
||||
try {
|
||||
let display = dayjs().tz(timezone).format("Z");
|
||||
|
||||
result.push({
|
||||
name: `(UTC${display}) ${timezone}`,
|
||||
value: timezone,
|
||||
time: getTimezoneOffset(timezone),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
console.log("Skip this timezone")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
if (a.time > b.time) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.time > a.time) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
34
src/util.js
34
src/util.js
@@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0;
|
||||
exports.DOWN = 0;
|
||||
exports.UP = 1;
|
||||
exports.PENDING = 2;
|
||||
function flipStatus(s) {
|
||||
if (s === exports.UP) {
|
||||
return exports.DOWN;
|
||||
}
|
||||
if (s === exports.DOWN) {
|
||||
return exports.UP;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
exports.flipStatus = flipStatus;
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
exports.sleep = sleep;
|
||||
function ucfirst(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
const firstLetter = str.substr(0, 1);
|
||||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
exports.ucfirst = ucfirst;
|
||||
function debug(msg) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
exports.debug = debug;
|
43
src/util.ts
43
src/util.ts
@@ -1,43 +0,0 @@
|
||||
// Common Util for frontend and backend
|
||||
// Backend uses the compiled file util.js
|
||||
// Frontend uses util.ts
|
||||
// Need to run "tsc" to compile if there are any changes.
|
||||
|
||||
export const DOWN = 0;
|
||||
export const UP = 1;
|
||||
export const PENDING = 2;
|
||||
|
||||
export function flipStatus(s) {
|
||||
if (s === UP) {
|
||||
return DOWN;
|
||||
}
|
||||
|
||||
if (s === DOWN) {
|
||||
return UP;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP's ucfirst
|
||||
* @param str
|
||||
*/
|
||||
export function ucfirst(str) {
|
||||
if (! str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const firstLetter = str.substr(0, 1);
|
||||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
|
||||
export function debug(msg) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "commonjs",
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": false,
|
||||
"files.insertFinalNewline": true
|
||||
},
|
||||
"files": [
|
||||
"./server/util.ts"
|
||||
]
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ['ie > 11'],
|
||||
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
|
||||
})
|
||||
]
|
||||
})
|
Reference in New Issue
Block a user