mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-18 01:16:54 +08:00
Compare commits
1774 Commits
1.0.7
...
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 | ||
|
48a4ced9a5 | ||
|
221aad55de | ||
|
377d475e05 | ||
|
0c3c59df4e | ||
|
eba996b0f2 | ||
|
4d71e03039 | ||
|
2740f096c0 | ||
|
8ebaca4c5c | ||
|
fceb594442 | ||
|
5b9d3357aa | ||
|
5689b30985 | ||
|
44c8ca9da8 | ||
|
6f044de6e6 | ||
|
7877adf7a3 | ||
|
f0e5e9f463 | ||
|
0263cfa7e4 | ||
|
8b733592cb | ||
|
71fa55c218 | ||
|
ee071e41f5 | ||
|
6f868c9ec3 | ||
|
33d7f8645a | ||
|
c6a66fad79 | ||
|
9f0be5f531 | ||
|
7f42888546 | ||
|
642a711bcd | ||
|
659d83b13c | ||
|
4b93900866 | ||
|
204624bfe9 | ||
|
b7fbc2c0e6 | ||
|
2ebd79d037 | ||
|
3f84e5e8ab | ||
|
ab1fe2e2d1 | ||
|
67a4e949a2 | ||
|
15ee853fac | ||
|
d58be56cb9 | ||
|
00cc140acd | ||
|
63f0a36811 | ||
|
06377af7e5 | ||
|
60aa67892d | ||
|
17b58eac9a | ||
|
b9a6088f50 | ||
|
2b3a48995b | ||
|
e032072900 | ||
|
bcf2a319c2 | ||
|
cdaa0a54a4 | ||
|
47b19ea2f2 | ||
|
1006b37a67 | ||
|
b91e9ddb7a | ||
|
be22fcb87d | ||
|
5a053e5875 | ||
|
081abcb6a1 | ||
|
71af902a4e | ||
|
4b86c84c36 | ||
|
f9a10d1672 | ||
|
e6915d8964 | ||
|
063697c20a | ||
|
435e4faef3 | ||
|
1425d0e91a | ||
|
7dbec90c95 | ||
|
53a90347ca | ||
|
133c7230bc | ||
|
3666ebb931 | ||
|
6bce270f42 | ||
|
4a9690437f | ||
|
c8c2300483 | ||
|
ac0f418294 | ||
|
d54bc866b4 | ||
|
be1fc0c2b6 | ||
|
d97091af51 | ||
|
4c8fdd07d9 | ||
|
9648d700d7 | ||
|
8331e795e7 | ||
|
3c6af6d3f4 | ||
|
209fa83cff | ||
|
cc6f1d7487 | ||
|
36436ed4ef | ||
|
934b797623 | ||
|
0f0a6299c0 | ||
|
e6ca105600 | ||
|
ef45aedda5 | ||
|
7edd79a74d | ||
|
fade240c7f | ||
|
46337ec348 | ||
|
cafd2c7388 | ||
|
47d830db1f | ||
|
3a8fbff514 | ||
|
a93fd274fd | ||
|
3b45006567 | ||
|
720051a351 | ||
|
3dcbae0889 | ||
|
96242dce0d | ||
|
7acb265559 | ||
|
582fb2fe29 | ||
|
e3d4a896b1 | ||
|
9a1bf6006a | ||
|
ef41a32353 |
@@ -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,14 +0,0 @@
|
|||||||
/.idea
|
|
||||||
/dist
|
|
||||||
/node_modules
|
|
||||||
/data/kuma.db
|
|
||||||
/.do
|
|
||||||
**/.dockerignore
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/docker-compose*
|
|
||||||
**/Dockerfile*
|
|
||||||
LICENSE
|
|
||||||
README.md
|
|
||||||
.editorconfig
|
|
||||||
.vscode
|
|
@@ -1,18 +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
|
|
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: ⚠ Please go to "Discussions" Tab if you want to ask or share something
|
|
||||||
about: BUG REPORT ONLY HERE
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
BUG REPORT ONLY HERE
|
|
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.
|
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,10 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.idea
|
|
||||||
|
|
||||||
/data
|
|
||||||
!/data/.gitkeep
|
|
||||||
.vscode
|
|
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.
|
|
124
README.md
124
README.md
@@ -1,124 +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.
|
|
||||||
|
|
||||||
### One-click Deploy to DigitalOcean
|
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
|
||||||
|
|
||||||
Choose Cheapest Plan is enough. (US$ 5)
|
|
||||||
|
|
||||||
# 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.7 --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/wiki/%5BDev%5D-Setup-Development-Environment
|
|
||||||
|
|
||||||
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:
|
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,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
|
|
41
dockerfile
41
dockerfile
@@ -1,41 +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
|
|
||||||
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/
|
|
||||||
# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev
|
|
||||||
# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo
|
|
||||||
# Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev
|
|
||||||
# Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev
|
|
||||||
# Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev
|
|
||||||
# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six
|
|
||||||
# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six
|
|
||||||
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
|
||||||
RUN apk add --no-cache python3 py3-pip py3-six cargo
|
|
||||||
RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev python3-dev && \
|
|
||||||
pip3 install apprise && \
|
|
||||||
apk del .build-deps
|
|
||||||
RUN apprise --version
|
|
||||||
|
|
||||||
# New things add here
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
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>
|
|
3823
package-lock.json
generated
3823
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "uptime-kuma",
|
|
||||||
"version": "1.0.7",
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --host",
|
|
||||||
"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.7 --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.7 && npm install && npm run build",
|
|
||||||
"version-global-replace": "node extra/version-global-replace.js",
|
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@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",
|
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"http-graceful-shutdown": "^3.1.2",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
|
||||||
"nodemailer": "^6.6.3",
|
|
||||||
"password-hash": "^1.2.2",
|
|
||||||
"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": {
|
|
||||||
"@vitejs/plugin-legacy": "^1.4.4",
|
|
||||||
"@vitejs/plugin-vue": "^1.2.5",
|
|
||||||
"@vue/compiler-sfc": "^3.1.5",
|
|
||||||
"core-js": "^3.15.2",
|
|
||||||
"sass": "^1.35.2",
|
|
||||||
"vite": "^2.4.2"
|
|
||||||
}
|
|
||||||
}
|
|
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,119 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const {sleep} = require("./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 = 3;
|
|
||||||
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')
|
|
||||||
var timezone = require('dayjs/plugin/timezone')
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(timezone)
|
|
||||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* status:
|
|
||||||
* 0 = DOWN
|
|
||||||
* 1 = UP
|
|
||||||
*/
|
|
||||||
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,342 +0,0 @@
|
|||||||
|
|
||||||
const https = require('https');
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const utc = require('dayjs/plugin/utc')
|
|
||||||
var timezone = require('dayjs/plugin/timezone')
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(timezone)
|
|
||||||
const axios = require("axios");
|
|
||||||
const {debug, UP, DOWN, PENDING} = require("../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")
|
|
||||||
|
|
||||||
// Use Custom agent to disable session reuse
|
|
||||||
// https://github.com/nodejs/node/issues/3940
|
|
||||||
const customAgent = new https.Agent({
|
|
||||||
maxCachedSessions: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* status:
|
|
||||||
* 0 = DOWN
|
|
||||||
* 1 = UP
|
|
||||||
*/
|
|
||||||
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,
|
|
||||||
notificationIDList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
start(io) {
|
|
||||||
let previousBeat = null;
|
|
||||||
let retries = 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
let res = await axios.get(this.url, {
|
|
||||||
headers: { "User-Agent": "Uptime-Kuma" },
|
|
||||||
httpsAgent: customAgent,
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
retries = 0;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
|
||||||
retries++;
|
|
||||||
bean.status = PENDING;
|
|
||||||
}
|
|
||||||
bean.msg = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * ? -> 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} | Type: ${this.type}`)
|
|
||||||
} else {
|
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
getUrl() {
|
|
||||||
try {
|
|
||||||
return new URL(this.url);
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store TLS info to database
|
|
||||||
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,333 +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") {
|
|
||||||
var 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 {
|
|
||||||
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";
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
} else {
|
|
||||||
return bcrypt.compareSync(password, hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSHA1(hash) {
|
|
||||||
return (typeof hash === "string" && hash.startsWith("sha1"))
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.needRehash = function (hash) {
|
|
||||||
return isSHA1(hash);
|
|
||||||
}
|
|
@@ -1,118 +0,0 @@
|
|||||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
|
||||||
// Fixed on Windows
|
|
||||||
|
|
||||||
var 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) {
|
|
||||||
var self = this;
|
|
||||||
callback = callback || function(err, ms) {
|
|
||||||
if (err) return self.emit('error', err);
|
|
||||||
else return self.emit('result', ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
var _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() {
|
|
||||||
var stdout = this.stdout._stdout,
|
|
||||||
stderr = this.stderr._stderr,
|
|
||||||
ms;
|
|
||||||
|
|
||||||
if (stderr)
|
|
||||||
return callback(new Error(stderr));
|
|
||||||
else 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) {
|
|
||||||
var 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);
|
|
||||||
};
|
|
751
server/server.js
751
server/server.js
@@ -1,751 +0,0 @@
|
|||||||
console.log("Welcome to Uptime Kuma ")
|
|
||||||
console.log("Importing libraries")
|
|
||||||
const express = require('express');
|
|
||||||
const http = require('http');
|
|
||||||
const { Server } = require("socket.io");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const {R} = require("redbean-node");
|
|
||||||
const passwordHash = require('./password-hash');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const Monitor = require("./model/monitor");
|
|
||||||
const fs = require("fs");
|
|
||||||
const {getSettings} = require("./util-server");
|
|
||||||
const {Notification} = require("./notification")
|
|
||||||
const gracefulShutdown = require('http-graceful-shutdown');
|
|
||||||
const Database = require("./database");
|
|
||||||
const {sleep} = require("./util");
|
|
||||||
const args = require('args-parser')(process.argv);
|
|
||||||
|
|
||||||
const version = require('../package.json').version;
|
|
||||||
const hostname = args.host || "0.0.0.0"
|
|
||||||
const 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;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await initDatabase();
|
|
||||||
|
|
||||||
console.log("Adding route")
|
|
||||||
app.use('/', express.static("dist"));
|
|
||||||
|
|
||||||
app.get('*', function(request, response, next) {
|
|
||||||
response.sendFile(process.cwd() + '/dist/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 R.findOne("user", " username = ? AND active = 1 ", [
|
|
||||||
data.username
|
|
||||||
])
|
|
||||||
|
|
||||||
if (user && passwordHash.verify(data.password, user.password)) {
|
|
||||||
|
|
||||||
// Upgrade the hash to bcrypt
|
|
||||||
if (passwordHash.needRehash(user.password)) {
|
|
||||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
|
||||||
passwordHash.generate(data.password),
|
|
||||||
user.id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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 (type, callback) => {
|
|
||||||
try {
|
|
||||||
checkLogin(socket)
|
|
||||||
|
|
||||||
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
data: await getSettings(type),
|
|
||||||
});
|
|
||||||
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const startGracefulShutdown = async () => {
|
|
||||||
console.log('Shutdown requested');
|
|
||||||
|
|
||||||
|
|
||||||
await (new Promise((resolve) => {
|
|
||||||
server.close(async function () {
|
|
||||||
console.log('Stopped Express.');
|
|
||||||
process.exit(0)
|
|
||||||
setTimeout(async () =>{
|
|
||||||
await R.close();
|
|
||||||
console.log("Stopped DB")
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function shutdownFunction(signal) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalFunction() {
|
|
||||||
console.log('Graceful Shutdown')
|
|
||||||
}
|
|
||||||
|
|
||||||
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,121 +0,0 @@
|
|||||||
const tcpp = require('tcp-ping');
|
|
||||||
const Ping = require("./ping-lite");
|
|
||||||
const {R} = require("redbean-node");
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
|
||||||
key
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = value;
|
|
||||||
await R.store(bean)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getSettings = async function (type) {
|
|
||||||
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
|
|
||||||
type
|
|
||||||
])
|
|
||||||
|
|
||||||
let result = {};
|
|
||||||
|
|
||||||
for (let row of list) {
|
|
||||||
result[row.key] = row.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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,26 +0,0 @@
|
|||||||
// Common JS cannot be used in frontend sadly
|
|
||||||
// sleep, ucfirst is duplicated in ../src/util-frontend.js
|
|
||||||
|
|
||||||
exports.DOWN = 0;
|
|
||||||
exports.UP = 1;
|
|
||||||
exports.PENDING = 2;
|
|
||||||
|
|
||||||
exports.sleep = function (ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.ucfirst = function (str) {
|
|
||||||
if (! str) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLetter = str.substr(0, 1);
|
|
||||||
return firstLetter.toUpperCase() + str.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.debug = (msg) => {
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
13
src/App.vue
13
src/App.vue
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -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,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="modal fade" tabindex="-1" ref="modal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="exampleModalLabel">Confirm</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Modal } from 'bootstrap'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
btnStyle: {
|
|
||||||
type: String,
|
|
||||||
default: "btn-primary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: () => ({
|
|
||||||
modal: null
|
|
||||||
}),
|
|
||||||
mounted() {
|
|
||||||
this.modal = new Modal(this.$refs.modal)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
show() {
|
|
||||||
this.modal.show()
|
|
||||||
},
|
|
||||||
yes() {
|
|
||||||
this.$emit('yes');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-if="isNum" ref="output">{{ output }}</span> <span v-if="isNum">{{ unit }}</span>
|
|
||||||
<span v-else>{{ value }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import {sleep} from '../util-frontend'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: [String, Number],
|
|
||||||
time: {
|
|
||||||
Number,
|
|
||||||
default: 0.3,
|
|
||||||
},
|
|
||||||
unit: {
|
|
||||||
String,
|
|
||||||
default: "ms",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.output = this.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
output: "",
|
|
||||||
frameDuration: 30,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,41 +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);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,181 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wrap" :style="wrapStyle" ref="wrap">
|
|
||||||
<div class="hp-bar-big" :style="barStyle">
|
|
||||||
<div
|
|
||||||
class="beat"
|
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
|
||||||
:style="beatStyle"
|
|
||||||
v-for="(beat, index) in shortBeatList"
|
|
||||||
:key="index"
|
|
||||||
:title="beat.msg">
|
|
||||||
</div>
|
|
||||||
</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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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);
|
|
||||||
|
|
||||||
let width
|
|
||||||
if (this.maxBeat > 0) {
|
|
||||||
width = (this.beatWidth + this.beatMargin * 2) * this.maxBeat + (leftRight * 2) + "px"
|
|
||||||
} {
|
|
||||||
width = "100%"
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
padding: `${topBottom}px ${leftRight}px`,
|
|
||||||
width: width
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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)`,
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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,77 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="form-container">
|
|
||||||
<div class="form">
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
|
|
||||||
<h1 class="h3 mb-3 fw-normal"></h1>
|
|
||||||
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username">
|
|
||||||
<label for="floatingInput">Username</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="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 type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
|
||||||
|
|
||||||
<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 class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok">
|
|
||||||
{{ 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,471 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="type" class="form-label">Notification Type</label>
|
|
||||||
<select class="form-select" id="type" v-model="notification.type">
|
|
||||||
<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="apprise">Apprise (Support 50+ Notification services)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Friendly Name</label>
|
|
||||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="notification.type === 'telegram'">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
|
||||||
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken">
|
|
||||||
<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 type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">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 type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
|
||||||
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" 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 type="text" class="form-control" id="hostname" required v-model="notification.smtpHost">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="port" class="form-label">Port</label>
|
|
||||||
<input type="number" class="form-control" id="port" v-model="notification.smtpPort" required min="0" max="65535" step="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
|
||||||
<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 type="text" class="form-control" id="username" v-model="notification.smtpUsername" autocomplete="false">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="from-email" class="form-label">From Email</label>
|
|
||||||
<input type="email" class="form-control" id="from-email" required v-model="notification.smtpFrom" autocomplete="false">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="to-email" class="form-label">To Email</label>
|
|
||||||
<input type="email" class="form-control" id="to-email" required v-model="notification.smtpTo" 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 type="text" class="form-control" id="discord-webhook-url" required v-model="notification.discordWebhookUrl" 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 type="url" pattern="https?://.+" class="form-control" id="signal-url" required v-model="notification.signalURL">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="signal-number" class="form-label">Number</label>
|
|
||||||
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
|
||||||
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients">
|
|
||||||
|
|
||||||
<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 type="text" class="form-control" id="gotify-application-token" required v-model="notification.gotifyapplicationToken">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="gotify-priority" class="form-label">Priority</label>
|
|
||||||
<input type="number" class="form-control" id="gotify-priority" v-model="notification.gotifyPriority" 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 type="text" class="form-control" id="slack-webhook-url" required v-model="notification.slackwebhookURL">
|
|
||||||
<label for="slack-username" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername">
|
|
||||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
|
||||||
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo">
|
|
||||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
|
||||||
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel">
|
|
||||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
|
||||||
<input type="text" class="form-control" id="slack-button" v-model="notification.slackbutton">
|
|
||||||
<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 type="text" class="form-control" id="pushover-user" required v-model="notification.pushoveruserkey">
|
|
||||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
|
||||||
<input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken">
|
|
||||||
<label for="pushover-device" class="form-label">Device</label>
|
|
||||||
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice">
|
|
||||||
<label for="pushover-device" class="form-label">Message Title</label>
|
|
||||||
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle">
|
|
||||||
<label for="pushover-priority" class="form-label">Priority</label>
|
|
||||||
<select class="form-select" id="pushover-priority" v-model="notification.pushoverpriority">
|
|
||||||
<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 class="form-select" id="pushover-sound" v-model="notification.pushoversounds">
|
|
||||||
<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 type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL">
|
|
||||||
<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 class="text-primary" v-if="appriseInstalled">Apprise is installed</span>
|
|
||||||
<span class="text-danger" v-else>Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button>
|
|
||||||
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button>
|
|
||||||
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<Confirm ref="confirmDelete" @yes="deleteNotification" btn-style="btn-danger">Are you sure want to delete this notification for all monitors?</Confirm>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Modal } from 'bootstrap'
|
|
||||||
import { ucfirst } from '../util-frontend'
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
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)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,43 +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"
|
|
||||||
} else if (this.status === 1) {
|
|
||||||
return "primary"
|
|
||||||
} else if (this.status === 2) {
|
|
||||||
return "warning"
|
|
||||||
} else {
|
|
||||||
return "secondary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
text() {
|
|
||||||
if (this.status === 0) {
|
|
||||||
return "Down"
|
|
||||||
} else if (this.status === 1) {
|
|
||||||
return "Up"
|
|
||||||
} else if (this.status === 2) {
|
|
||||||
return "Pending"
|
|
||||||
} else {
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
span {
|
|
||||||
width: 54px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span :class="className">{{ uptime }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
monitor : Object,
|
|
||||||
type: String,
|
|
||||||
pill: {
|
|
||||||
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 + "%";
|
|
||||||
} else {
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
color() {
|
|
||||||
if (this.lastHeartBeat.status === 0) {
|
|
||||||
return "danger"
|
|
||||||
} else if (this.lastHeartBeat.status === 1) {
|
|
||||||
return "primary"
|
|
||||||
} else if (this.lastHeartBeat.status === 2) {
|
|
||||||
return "warning"
|
|
||||||
} else {
|
|
||||||
return "secondary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
lastHeartBeat() {
|
|
||||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
|
||||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
|
||||||
} else {
|
|
||||||
return { status: -1 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
className() {
|
|
||||||
if (this.pill) {
|
|
||||||
return `badge rounded-pill bg-${this.color}`;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,152 +0,0 @@
|
|||||||
<template>
|
|
||||||
|
|
||||||
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
|
|
||||||
<div class="container-fluid">
|
|
||||||
Lost connection to the socket server. Reconnecting...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop header -->
|
|
||||||
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile">
|
|
||||||
<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"></object>
|
|
||||||
<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">📊 Dashboard</router-link></li>
|
|
||||||
<li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Mobile header -->
|
|
||||||
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else>
|
|
||||||
<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"></object>
|
|
||||||
<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 style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
|
|
||||||
<nav class="bottom-nav" v-if="$root.isMobile">
|
|
||||||
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link>
|
|
||||||
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a>
|
|
||||||
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div>➕</div>Add</router-link>
|
|
||||||
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Login from "../components/Login.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Login
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route (to, from) {
|
|
||||||
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 35px;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
color: #AAA;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
margin-left: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
92
src/main.js
92
src/main.js
@@ -1,92 +0,0 @@
|
|||||||
import {createApp, h} from "vue";
|
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
|
||||||
|
|
||||||
import App from './App.vue'
|
|
||||||
import Layout from './layouts/Layout.vue'
|
|
||||||
import EmptyLayout from './layouts/EmptyLayout.vue'
|
|
||||||
import Settings from "./pages/Settings.vue";
|
|
||||||
import Dashboard from "./pages/Dashboard.vue";
|
|
||||||
import DashboardHome from "./pages/DashboardHome.vue";
|
|
||||||
import Details from "./pages/Details.vue";
|
|
||||||
import socket from "./mixins/socket"
|
|
||||||
import "./assets/app.scss"
|
|
||||||
import EditMonitor from "./pages/EditMonitor.vue";
|
|
||||||
import Toast from "vue-toastification";
|
|
||||||
import "vue-toastification/dist/index.css";
|
|
||||||
import "bootstrap"
|
|
||||||
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.mount('#app')
|
|
||||||
|
|
@@ -1,327 +0,0 @@
|
|||||||
import {io} from "socket.io-client";
|
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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("connect_error", (err) => {
|
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('info', (info) => {
|
|
||||||
this.info = info;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('setup', (monitorID, data) => {
|
|
||||||
this.$router.push("/setup")
|
|
||||||
});
|
|
||||||
|
|
||||||
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('disconnect', () => {
|
|
||||||
console.log("disconnect")
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.storage().token) {
|
|
||||||
this.loginByToken(this.storage().token)
|
|
||||||
} 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()
|
|
||||||
} else {
|
|
||||||
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,150 +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">Add New Monitor</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow-box list mb-4" v-if="showList">
|
|
||||||
|
|
||||||
<div class="text-center mt-3" v-if="Object.keys($root.monitorList).length === 0">
|
|
||||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index">
|
|
||||||
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
min-width: 58px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-padding {
|
|
||||||
padding-left: 5px !important;
|
|
||||||
padding-right: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,187 +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 class="row" v-if="false">
|
|
||||||
<div class="col-3">
|
|
||||||
<h3>Uptime</h3>
|
|
||||||
<p>(24-hour)</p>
|
|
||||||
<span class="num"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<h3>Uptime</h3>
|
|
||||||
<p>(30-day)</p>
|
|
||||||
<span class="num"></span>
|
|
||||||
</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;
|
|
||||||
} else if (a.time < b.time) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
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,332 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h1> {{ monitor.name }}</h1>
|
|
||||||
<p class="url">
|
|
||||||
<a :href="monitor.url" target="_blank" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">{{ 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 class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
|
|
||||||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
|
||||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
|
|
||||||
<button class="btn btn-danger" @click="deleteDialog">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 class="col" v-if="certInfo">
|
|
||||||
<h4>CertExp.</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 class="shadow-box big-padding text-center" v-if="showCertInfoBox">
|
|
||||||
<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" btnStyle="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,
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
page: 1,
|
|
||||||
perPage: 25,
|
|
||||||
heartBeatList: [],
|
|
||||||
toggleCertInfoBox: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
|
|
||||||
pingTitle() {
|
|
||||||
if (this.monitor.type === "http") {
|
|
||||||
return "Response"
|
|
||||||
} else {
|
|
||||||
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]
|
|
||||||
} else {
|
|
||||||
return { status: -1 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ping() {
|
|
||||||
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
|
|
||||||
return this.lastHeartBeat.ping;
|
|
||||||
} else {
|
|
||||||
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];
|
|
||||||
} else {
|
|
||||||
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]
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
status() {
|
|
||||||
if (this.$root.statusList[this.monitor.id]) {
|
|
||||||
return this.$root.statusList[this.monitor.id]
|
|
||||||
} else {
|
|
||||||
return { }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
certInfo() {
|
|
||||||
if (this.$root.certInfoList[this.monitor.id]) {
|
|
||||||
return this.$root.certInfoList[this.monitor.id]
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,178 +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 class="form-select" aria-label="Default select example" id="type" v-model="monitor.type">
|
|
||||||
<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 type="text" class="form-control" id="name" v-model="monitor.name" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
|
||||||
<label for="url" class="form-label">URL</label>
|
|
||||||
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'keyword' ">
|
|
||||||
<label for="keyword" class="form-label">Keyword</label>
|
|
||||||
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required>
|
|
||||||
<div class="form-text">Search keyword in plain html or JSON response and it is case-sensitive</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' ">
|
|
||||||
<label for="hostname" class="form-label">Hostname</label>
|
|
||||||
<input type="text" class="form-control" id="hostname" v-model="monitor.hostname" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'port' ">
|
|
||||||
<label for="port" class="form-label">Port</label>
|
|
||||||
<input type="number" class="form-control" id="port" v-model="monitor.port" 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 type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="maxRetries" class="form-label">Retries</label>
|
|
||||||
<input type="number" class="form-control" id="maxRetries" v-model="monitor.maxretries" 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>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
|
|
||||||
<div class="mt-3" v-if="$root.isMobile"></div>
|
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
|
||||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3" :key="notification.id" v-for="notification in $root.notificationList">
|
|
||||||
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
|
|
||||||
|
|
||||||
<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" @click="$refs.notificationDialog.show()" type="button">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
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
init() {
|
|
||||||
if (this.isAdd) {
|
|
||||||
console.log("??????")
|
|
||||||
this.monitor = {
|
|
||||||
type: "http",
|
|
||||||
name: "",
|
|
||||||
url: "https://",
|
|
||||||
interval: 60,
|
|
||||||
maxretries: 0,
|
|
||||||
notificationIDList: {},
|
|
||||||
}
|
|
||||||
} 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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$route.fullPath' () {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.shadow-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,145 +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 class="form-select" id="timezone" v-model="$root.userTimezone">
|
|
||||||
<option value="auto">Auto: {{ guessTimezone }}</option>
|
|
||||||
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary" type="submit">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<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 type="password" class="form-control" id="current-password" required v-model="password.currentPassword">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new-password" class="form-label">New Password</label>
|
|
||||||
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
|
||||||
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-danger" @click="$root.logout">Logout</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
|
|
||||||
<div class="mt-3" v-if="$root.isMobile"></div>
|
|
||||||
|
|
||||||
<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 class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index">
|
|
||||||
{{ notification.name }}<br />
|
|
||||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NotificationDialog ref="notificationDialog" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
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
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
timezoneList: timezoneList(),
|
|
||||||
guessTimezone: dayjs.tz.guess(),
|
|
||||||
|
|
||||||
invalidPassword: false,
|
|
||||||
password: {
|
|
||||||
currentPassword: "",
|
|
||||||
newPassword: "",
|
|
||||||
repeatNewPassword: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
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 = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
"password.repeatNewPassword"() {
|
|
||||||
this.invalidPassword = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.shadow-box {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="form-container">
|
|
||||||
<div class="form">
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<object width="64" height="64" data="/icon.svg"></object>
|
|
||||||
<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 type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username" required>
|
|
||||||
<label for="floatingInput">Username</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password" required>
|
|
||||||
<label for="floatingPassword">Password</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
|
||||||
<input type="password" class="form-control" id="repeat" placeholder="Repeat Password" v-model="repeatPassword" 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,415 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import utc from 'dayjs/plugin/utc'
|
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
|
||||||
|
|
||||||
dayjs.extend(utc)
|
|
||||||
dayjs.extend(timezone)
|
|
||||||
|
|
||||||
export function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ucfirst(str) {
|
|
||||||
if (! str) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLetter = str.substr(0, 1);
|
|
||||||
return firstLetter.toUpperCase() + str.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
} else if (b.time > a.time) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
@@ -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