mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-10 20:57:19 +08:00
Compare commits
651 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6a603203cc | ||
|
4b8e7fcffc | ||
|
7fd12f5485 | ||
|
0d87a6dfea | ||
|
2082c3f68a | ||
|
ea9f434553 | ||
|
fa1216c198 | ||
|
6d8aa20fc6 | ||
|
5430da955a | ||
|
e6e2b0ebf8 | ||
|
445dc486be | ||
|
d2151737c1 | ||
|
78fc6de542 | ||
|
c15e6631ae | ||
|
f84135ca67 | ||
|
80eca886ce | ||
|
274a9050c0 | ||
|
1f4ad938b1 | ||
|
312aedf391 | ||
|
4f28bfbde3 | ||
|
ee76ab4ec2 | ||
|
18616ee590 | ||
|
19a4d570ec | ||
|
79fda8f442 | ||
|
53a14cf4f5 | ||
|
2241d8817f | ||
|
3831dfe0b9 | ||
|
fdde862549 | ||
|
e31be8caf5 | ||
|
60f2f08cea | ||
|
b1647a310e | ||
|
23f1a73fc8 | ||
|
d2bf2a551d | ||
|
f23ecef636 | ||
|
51cf2ff6f9 | ||
|
b30b1d3a52 | ||
|
582e14098d | ||
|
6e3e2fc85c | ||
|
b604807cfe | ||
|
3ee13bddd1 | ||
|
c74986647e | ||
|
dac410c850 | ||
|
b88b357b55 | ||
|
9a5cd5172b | ||
|
941788db49 | ||
|
99725aabe7 | ||
|
9d41da4aa2 | ||
|
a0f372e946 | ||
|
877ad1438f | ||
|
9116654a33 | ||
|
dbc70bc5ed | ||
|
a29b65e801 | ||
|
bd9568ce5b | ||
|
7a109689d9 | ||
|
e7929f461d | ||
|
a2cf7f394e | ||
|
e1f378ee6c | ||
|
eeb00a5511 | ||
|
129dc3324b | ||
|
268d2e2353 | ||
|
7d2cf0ee57 | ||
|
2d408732db | ||
|
0cc5053f14 | ||
|
1c4e5b79be | ||
|
4aae402b36 | ||
|
dce2ba8f9f | ||
|
ed5c75282c | ||
|
00ac560bd6 | ||
|
8ea4dec5a0 | ||
|
3006d13aee | ||
|
c7e6cb9f37 | ||
|
9b82bb248e | ||
|
e55cf65f28 | ||
|
f24002838c | ||
|
b4fd4f7d90 | ||
|
3a3f1762ac | ||
|
6376d4eb92 | ||
|
e37cf9b1a7 | ||
|
3b0191210b | ||
|
923d325b44 | ||
|
ad38e61c26 | ||
|
22095c09b1 | ||
|
a5e62141d5 | ||
|
e4b76717be | ||
|
bc70ecfba7 | ||
|
f1238ab762 | ||
|
cd1a3a2fb9 | ||
|
25db162721 | ||
|
3e7bb355fd | ||
|
697fa6bdfd | ||
|
527e0c3444 | ||
|
a41534ca60 | ||
|
fa549cb80e | ||
|
0127d5102e | ||
|
ec731d174d | ||
|
0d65918a6a | ||
|
21db31bb30 | ||
|
3ee0addb1f | ||
|
8c5d1945be | ||
|
bb799163e8 | ||
|
bf29f28726 | ||
|
22db9c9b8a | ||
|
96ff70faaa | ||
|
2776f942ab | ||
|
14b9afb400 | ||
|
3ad736692f | ||
|
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 | ||
|
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 | ||
|
4d7c2d329b | ||
|
1982e2f8b8 | ||
|
2819094377 | ||
|
06c4523ce3 | ||
|
ac3732f6cc | ||
|
bf3e9dccd2 | ||
|
5b18a6a518 | ||
|
caec933186 | ||
|
51ac7a58dc | ||
|
db26b7d123 | ||
|
7b8459c73a | ||
|
d0c63ebe3e | ||
|
803f0d6219 | ||
|
d556509d07 | ||
|
3cc4955cad | ||
|
48f82b55f8 | ||
|
280ba84aca | ||
|
0dbecca10f | ||
|
8279368b4d | ||
|
2450b3d082 | ||
|
6b72d5033a | ||
|
4d262bbb6a | ||
|
d1370a62bd | ||
|
063fd6ef43 | ||
|
1d4d7fa9c4 | ||
|
248b5292dc | ||
|
47d830db1f | ||
|
3a8fbff514 | ||
|
a93fd274fd | ||
|
77fbfc23be | ||
|
3b45006567 | ||
|
b7a32d4ab6 | ||
|
5a219554b3 | ||
|
70b1f197c1 | ||
|
720051a351 | ||
|
32a5e838ba | ||
|
86e18ac11d | ||
|
3dcbae0889 | ||
|
96242dce0d | ||
|
f20ab4b0e3 | ||
|
96c60dd94a | ||
|
7acb265559 | ||
|
582fb2fe29 | ||
|
e3d4a896b1 | ||
|
9a1bf6006a | ||
|
ef41a32353 | ||
|
ccda6f05f5 | ||
|
03b3bb5b30 | ||
|
7e4a1ad279 | ||
|
916b9da0dc | ||
|
a64ce81457 | ||
|
c575afc8e0 | ||
|
1e42343aee | ||
|
afd4cf2425 | ||
|
e02eb72863 | ||
|
1c0dc18d72 | ||
|
c00612c1a9 | ||
|
32345fcbe9 | ||
|
fd90458e77 | ||
|
d89e6f4649 | ||
|
c4ca8e2acb | ||
|
94b5a557bf | ||
|
14e1d1f105 | ||
|
8b905b6b12 | ||
|
fa57d40c3c | ||
|
62e231e92b | ||
|
02b4dfc100 | ||
|
657acf748b | ||
|
054269ecf0 | ||
|
71dd68bb6d | ||
|
02230930c5 | ||
|
a8b102ad4a | ||
|
58d029445d | ||
|
77af41bfff | ||
|
058032a26a | ||
|
cbb9d3f91b | ||
|
5bd3184ebf | ||
|
59ebe134f1 | ||
|
851ceef3d5 | ||
|
efd7608ba2 | ||
|
05fdaf0c96 | ||
|
01b0e82d52 | ||
|
d2ccfd5366 | ||
|
7cba9ce231 | ||
|
69e8c56e3e | ||
|
9928ea8c30 | ||
|
25c370c9ff | ||
|
9227ff6ea3 | ||
|
2d943620c7 | ||
|
f7bd67c413 | ||
|
9ca2444dab | ||
|
1d45a7606d | ||
|
16f363ac38 | ||
|
d6b9403f60 | ||
|
92e5ddd97c | ||
|
23611e540c | ||
|
f9274557f3 | ||
|
44b66cbd2e | ||
|
66037e236c | ||
|
386c002fda | ||
|
7c1aab6a15 | ||
|
37884cfd08 | ||
|
ce6841eae7 | ||
|
7c94c3b502 | ||
|
268c8e50f5 | ||
|
13c9244e3f | ||
|
13b3a5be9c | ||
|
2e31e780a1 | ||
|
6a2b5f9dd8 | ||
|
16767dc042 | ||
|
fb3e000dc3 | ||
|
6f3ea21864 | ||
|
403137280e | ||
|
d94894b7e0 | ||
|
a173700cd4 | ||
|
9c248776e7 | ||
|
309caa4279 | ||
|
28b14ceb70 | ||
|
9f9c42c30b | ||
|
2bff62cade | ||
|
bfb4a5bcd4 | ||
|
e87b78501b | ||
|
637422494f | ||
|
db34484ff2 | ||
|
790c071f2c | ||
|
149688e669 | ||
|
7dae5279fb | ||
|
c203317b3b | ||
|
01f2fccb23 | ||
|
7808aef58f | ||
|
ce2d78f45a | ||
|
20cad50593 | ||
|
829a2a191d | ||
|
65b320d06b | ||
|
1935da5b16 | ||
|
78f5d2cd8b | ||
|
f62b70c9a9 | ||
|
dfa9b3a0ca | ||
|
b3bff8d735 | ||
|
f2af5bc064 | ||
|
91b736f391 | ||
|
275f77d4bb | ||
|
b00524067a | ||
|
25a93b05dc | ||
|
53e203d2f9 | ||
|
60493f0f86 | ||
|
63c6e29e62 | ||
|
5f6d5588a6 | ||
|
18744d834f | ||
|
8dd5b97b79 |
@@ -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,5 +1,37 @@
|
||||
/.idea
|
||||
/dist
|
||||
/node_modules
|
||||
/data/kuma.db
|
||||
/data
|
||||
/.do
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/docker-compose*
|
||||
**/[Dd]ockerfile*
|
||||
LICENSE
|
||||
README.md
|
||||
.editorconfig
|
||||
.vscode
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.github
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
app.json
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
#node_modules
|
||||
.DS_Store
|
||||
#dist
|
||||
dist-ssr
|
||||
*.local
|
||||
#.idea
|
||||
|
||||
#/data
|
||||
#!/data/.gitkeep
|
||||
#.vscode
|
||||
|
||||
### End of .gitignore content
|
||||
|
@@ -16,3 +16,6 @@ indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.vue]
|
||||
trim_trailing_whitespace = false
|
||||
|
84
.eslintrc.js
Normal file
84
.eslintrc.js
Normal file
@@ -0,0 +1,84 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@babel/eslint-parser",
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
},
|
||||
rules: {
|
||||
"camelcase": ["warn", {
|
||||
"properties": "never"
|
||||
}],
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
"no-unused-vars": "warn",
|
||||
indent: [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
ignoredNodes: ["TemplateLiteral"],
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
quotes: ["warn", "double"],
|
||||
//semi: ['off', 'never'],
|
||||
"vue/html-indent": ["warn", 4], // default: 2
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"no-multi-spaces": ["error", {
|
||||
ignoreEOLComments: true,
|
||||
}],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "always",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"curly": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-curly-newline": "off",
|
||||
"object-property-newline": "error",
|
||||
"comma-spacing": "error",
|
||||
"brace-style": "error",
|
||||
"no-var": "error",
|
||||
"key-spacing": "warn",
|
||||
"keyword-spacing": "warn",
|
||||
"space-infix-ops": "warn",
|
||||
"arrow-spacing": "warn",
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-constant-condition": ["error", {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
"space-before-blocks": "warn",
|
||||
//'no-console': 'warn',
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-multiple-empty-lines": ["warn", {
|
||||
"max": 1,
|
||||
"maxBOF": 0,
|
||||
}],
|
||||
"lines-between-class-members": ["warn", "always", {
|
||||
exceptAfterSingleLine: true,
|
||||
}],
|
||||
"no-unneeded-ternary": "error",
|
||||
"array-bracket-newline": ["error", "consistent"],
|
||||
"eol-last": ["error", "always"],
|
||||
//'prefer-template': 'error',
|
||||
"comma-dangle": ["warn", "only-multiline"],
|
||||
"no-empty": ["error", {
|
||||
"allowEmptyCatch": true
|
||||
}],
|
||||
"no-control-regex": "off",
|
||||
"one-var": ["error", "never"],
|
||||
"max-statements-per-line": ["error", { "max": 1 }]
|
||||
},
|
||||
}
|
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
#patreon: # Replace with a single Patreon username
|
||||
open_collective: uptime-kuma # Replace with a single Open Collective username
|
||||
#ko_fi: # Replace with a single Ko-fi username
|
||||
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
#liberapay: # Replace with a single Liberapay username
|
||||
#issuehunt: # Replace with a single IssueHunt username
|
||||
#otechie: # Replace with a single Otechie username
|
||||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
17
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Ask for help
|
||||
about: You can ask any question related to Uptime Kuma.
|
||||
title: ''
|
||||
labels: help
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
OS:
|
||||
Browser:
|
||||
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
**Info**
|
||||
- Uptime Kuma Version:
|
||||
- Using Docker?: Yes/No
|
||||
- OS:
|
||||
- Browser:
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Error Log**
|
||||
It is easier for us to find out the problem.
|
||||
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ dist-ssr
|
||||
|
||||
/data
|
||||
!/data/.gitkeep
|
||||
.vscode
|
9
.stylelintrc
Normal file
9
.stylelintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"no-descending-specificity": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"declaration-empty-line-before": null
|
||||
}
|
||||
}
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
louis@uptimekuma.louislam.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
152
CONTRIBUTING.md
Normal file
152
CONTRIBUTING.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Project Info
|
||||
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
|
||||
# Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
|
||||
|
||||
If you are not sure, feel free to create an empty pull request draft first.
|
||||
|
||||
## Pull Request Examples
|
||||
|
||||
### ✅ High - Medium Priority
|
||||
|
||||
- Add a new notification
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
|
||||
### *️⃣ Requires one more reviewer
|
||||
|
||||
I do not have such knowledge to test it.
|
||||
|
||||
- Add k8s supports
|
||||
|
||||
### *️⃣ Low Priority
|
||||
|
||||
It changed my current workflow and require further studies.
|
||||
|
||||
- Change my release approach
|
||||
|
||||
### ❌ Won't Merge
|
||||
|
||||
- Duplicated pull request
|
||||
- Buggy
|
||||
- Existing logic is completely modified or deleted
|
||||
- A function that is completely out of scope
|
||||
|
||||
# Project Styles
|
||||
|
||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||
- All settings in frontend.
|
||||
- Easy to use
|
||||
|
||||
# Coding Styles
|
||||
|
||||
- Follow .editorconfig
|
||||
- Follow eslint
|
||||
|
||||
## Name convention
|
||||
|
||||
- Javascript/Typescript: camelCaseType
|
||||
- SQLite: underscore_type
|
||||
- CSS/SCSS: dash-type
|
||||
|
||||
# Tools
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
|
||||
# Install dependencies
|
||||
|
||||
```bash
|
||||
npm install --dev
|
||||
```
|
||||
|
||||
For npm@7, you need --legacy-peer-deps
|
||||
|
||||
```
|
||||
npm install --legacy-peer-deps --dev
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
|
||||
```bash
|
||||
npm run start-server
|
||||
|
||||
# Or
|
||||
|
||||
node server/server.js
|
||||
```
|
||||
|
||||
It binds to 0.0.0.0:3001 by default.
|
||||
|
||||
|
||||
## Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
|
||||
# Frontend Dev
|
||||
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
|
||||
|
||||
You can use Vue Devtool Chrome extension for debugging.
|
||||
|
||||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
|
||||
|
||||
```javascript
|
||||
localStorage.dev = "dev";
|
||||
```
|
||||
|
||||
So that the frontend will try to connect websocket server in 3001.
|
||||
|
||||
Alternately, you can specific NODE_ENV to "development".
|
||||
|
||||
|
||||
## Build the frontend
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Frontend Details
|
||||
|
||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||
|
||||
The router in "src/main.js"
|
||||
|
||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
||||
|
||||
The data and socket logic in "src/mixins/socket.js"
|
||||
|
||||
# Database Migration
|
||||
|
||||
1. create `patch{num}.sql` in `./db/`
|
||||
1. update `latestVersion` in `./server/database.js`
|
||||
|
||||
# Unit Test
|
||||
|
||||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.
|
||||
|
||||
|
||||
|
||||
|
||||
|
100
README.md
100
README.md
@@ -2,7 +2,6 @@
|
||||
|
||||
<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>
|
||||
@@ -11,38 +10,38 @@ It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
|
||||
|
||||
# Features
|
||||
## 🥔 Live Demo
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping.
|
||||
Try it!
|
||||
|
||||
https://demo.uptime.kuma.pet
|
||||
|
||||
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
|
||||
|
||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Webhook, Telegram, Discord and email (SMTP).
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
||||
* 20 seconds interval.
|
||||
|
||||
# How to Use
|
||||
## 🔧 How to Install
|
||||
|
||||
### Docker
|
||||
|
||||
⚠ For someone, who are using Raspberry Pi 3<=, please keep using 1.0.1.
|
||||
### 🐳 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
|
||||
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
|
||||
### 💪🏻 Without Docker
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma
|
||||
```
|
||||
|
||||
### Without Docker
|
||||
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/louislam/uptime-kuma.git
|
||||
@@ -50,45 +49,43 @@ cd uptime-kuma
|
||||
npm run setup
|
||||
|
||||
# Option 1. Try it
|
||||
npm run start-server
|
||||
node server/server.js
|
||||
|
||||
# (Recommended)
|
||||
# Option 2. Run in background using PM2
|
||||
# (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
|
||||
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
|
||||
### One-click Deploy to DigitalOcean
|
||||
### Advanced Installation
|
||||
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
||||
If you need more options or need to browse via a reserve proxy, please read:
|
||||
|
||||
Choose Cheapest Plan is enough. (US$ 5)
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||
|
||||
# How to Update
|
||||
|
||||
### Docker
|
||||
## 🆙 How to Update
|
||||
|
||||
Re-pull the latest docker image and create another container with the same volume.
|
||||
Please read:
|
||||
|
||||
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
|
||||
|
||||
### Without Docker
|
||||
## 🆕 What's Next?
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git checkout 1.0.4 --force
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart uptime-kuma
|
||||
```
|
||||
I will mark requests/issues to the next milestone.
|
||||
|
||||
# More Screenshots
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
|
||||
Project Plan:
|
||||
|
||||
https://github.com/louislam/uptime-kuma/projects/1
|
||||
|
||||
## 🖼 More Screenshots
|
||||
|
||||
Dark Mode:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
@@ -98,16 +95,21 @@ Telegram Notification Sample:
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
|
||||
|
||||
## Motivation
|
||||
|
||||
# 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.
|
||||
* 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 ones 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.
|
||||
* 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 may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
||||
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
||||
|
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
https://github.com/louislam/uptime-kuma/issues
|
BIN
db/demo_kuma.db
Normal file
BIN
db/demo_kuma.db
Normal file
Binary file not shown.
BIN
db/kuma.db
BIN
db/kuma.db
Binary file not shown.
37
db/patch1.sql
Normal file
37
db/patch1.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- 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;
|
9
db/patch2.sql
Normal file
9
db/patch2.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE monitor_tls_info (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
info_json TEXT
|
||||
);
|
||||
|
||||
COMMIT;
|
37
db/patch3.sql
Normal file
37
db/patch3.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- 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;
|
40
db/patch4.sql
Normal file
40
db/patch4.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- OK.... serious wrong, missing maxretries column
|
||||
-- Developers should patch it manually if you have missing the maxretries column
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table monitor_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
name VARCHAR(150),
|
||||
active BOOLEAN default 1 not null,
|
||||
user_id INTEGER
|
||||
references user
|
||||
on update cascade on delete set null,
|
||||
interval INTEGER default 20 not null,
|
||||
url TEXT,
|
||||
type VARCHAR(20),
|
||||
weight INTEGER default 2000,
|
||||
hostname VARCHAR(255),
|
||||
port INTEGER,
|
||||
created_date DATETIME,
|
||||
keyword VARCHAR(255),
|
||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||
ignore_tls BOOLEAN default 0 not null,
|
||||
upside_down BOOLEAN default 0 not null
|
||||
);
|
||||
|
||||
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
70
db/patch5.sql
Normal file
70
db/patch5.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
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 default (DATETIME('now')) not null,
|
||||
keyword VARCHAR(255),
|
||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||
ignore_tls BOOLEAN default 0 not null,
|
||||
upside_down BOOLEAN default 0 not null
|
||||
);
|
||||
|
||||
insert into
|
||||
monitor_dg_tmp(
|
||||
id,
|
||||
name,
|
||||
active,
|
||||
user_id,
|
||||
interval,
|
||||
url,
|
||||
type,
|
||||
weight,
|
||||
hostname,
|
||||
port,
|
||||
keyword,
|
||||
maxretries,
|
||||
ignore_tls,
|
||||
upside_down
|
||||
)
|
||||
select
|
||||
id,
|
||||
name,
|
||||
active,
|
||||
user_id,
|
||||
interval,
|
||||
url,
|
||||
type,
|
||||
weight,
|
||||
hostname,
|
||||
port,
|
||||
keyword,
|
||||
maxretries,
|
||||
ignore_tls,
|
||||
upside_down
|
||||
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;
|
74
db/patch6.sql
Normal file
74
db/patch6.sql
Normal file
@@ -0,0 +1,74 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
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 default (DATETIME('now')) not null,
|
||||
keyword VARCHAR(255),
|
||||
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||
ignore_tls BOOLEAN default 0 not null,
|
||||
upside_down BOOLEAN default 0 not null,
|
||||
maxredirects INTEGER default 10 not null,
|
||||
accepted_statuscodes_json TEXT default '["200-299"]' not null
|
||||
);
|
||||
|
||||
insert into
|
||||
monitor_dg_tmp(
|
||||
id,
|
||||
name,
|
||||
active,
|
||||
user_id,
|
||||
interval,
|
||||
url,
|
||||
type,
|
||||
weight,
|
||||
hostname,
|
||||
port,
|
||||
created_date,
|
||||
keyword,
|
||||
maxretries,
|
||||
ignore_tls,
|
||||
upside_down
|
||||
)
|
||||
select
|
||||
id,
|
||||
name,
|
||||
active,
|
||||
user_id,
|
||||
interval,
|
||||
url,
|
||||
type,
|
||||
weight,
|
||||
hostname,
|
||||
port,
|
||||
created_date,
|
||||
keyword,
|
||||
maxretries,
|
||||
ignore_tls,
|
||||
upside_down
|
||||
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;
|
10
db/patch7.sql
Normal file
10
db/patch7.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD dns_resolve_type VARCHAR(5);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD dns_resolve_server VARCHAR(255);
|
||||
|
||||
COMMIT;
|
7
db/patch8.sql
Normal file
7
db/patch8.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD dns_last_result VARCHAR(255);
|
||||
|
||||
COMMIT;
|
35
dockerfile
35
dockerfile
@@ -1,35 +1,26 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12
|
||||
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 && \
|
||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
|
||||
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.
|
||||
npm install mapbox/node-sqlite3#593c9d && \
|
||||
apk del .build-deps && \
|
||||
rm -f /usr/bin/python
|
||||
|
||||
# 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
|
||||
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
||||
RUN apk add --no-cache python3
|
||||
RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev cargo py3-pip python3-dev && \
|
||||
pip3 install apprise && \
|
||||
apk del .build-deps
|
||||
|
||||
# New things add here
|
||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
||||
RUN pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN npm install --legacy-peer-deps && npm run build && npm prune
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
CMD ["npm", "run", "start-server"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
2
extra/compile-install-script.ps1
Normal file
2
extra/compile-install-script.ps1
Normal file
@@ -0,0 +1,2 @@
|
||||
# Must enable File Sharing in Docker Desktop
|
||||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh
|
19
extra/healthcheck.js
Normal file
19
extra/healthcheck.js
Normal file
@@ -0,0 +1,19 @@
|
||||
let http = require("http");
|
||||
let options = {
|
||||
host: "localhost",
|
||||
port: "3001",
|
||||
timeout: 2000,
|
||||
};
|
||||
let 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();
|
245
extra/install.batsh
Normal file
245
extra/install.batsh
Normal file
@@ -0,0 +1,245 @@
|
||||
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
||||
// "npm run compile-install-script" to compile install.sh
|
||||
// The command is working on Windows PowerShell and Docker for Windows only.
|
||||
|
||||
|
||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
println("=====================");
|
||||
println("Uptime Kuma Installer");
|
||||
println("=====================");
|
||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
||||
println("---------------------------------------");
|
||||
println("This script is designed for Linux and basic usage.");
|
||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||
println("---------------------------------------");
|
||||
println("");
|
||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
||||
println("Docker - Install Uptime Kuma Docker container");
|
||||
println("");
|
||||
|
||||
if ("$1" != "") {
|
||||
type = "$1";
|
||||
} else {
|
||||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
|
||||
}
|
||||
|
||||
defaultPort = "3001";
|
||||
|
||||
function checkNode() {
|
||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||
println("Node Version: " ++ nodeVersion);
|
||||
|
||||
if (nodeVersion < "12") {
|
||||
println("Error: Required Node.js 14");
|
||||
call("exit", "1");
|
||||
}
|
||||
|
||||
if (nodeVersion == "12") {
|
||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
||||
}
|
||||
}
|
||||
|
||||
function deb() {
|
||||
bash("nodeCheck=$(node -v)");
|
||||
bash("apt --yes update");
|
||||
|
||||
if (nodeCheck != "") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
// Old nodejs binary name is "nodejs"
|
||||
bash("check=$(nodejs --version)");
|
||||
if (check != "") {
|
||||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("apt --yes install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("apt --yes install nodejs");
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
|
||||
if (nodeCheckAgain == "") {
|
||||
println("Error during Node.js installation");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
println("Installing Git");
|
||||
bash("apt --yes install git");
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "local") {
|
||||
defaultInstallPath = "/opt/uptime-kuma";
|
||||
|
||||
if (exists("/etc/redhat-release")) {
|
||||
os = call("cat", "/etc/redhat-release");
|
||||
distribution = "rhel";
|
||||
|
||||
} else if (exists("/etc/issue")) {
|
||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||
if (os == "Ubuntu") {
|
||||
distribution = "ubuntu";
|
||||
}
|
||||
if (os == "Debian") {
|
||||
distribution = "debian";
|
||||
}
|
||||
}
|
||||
|
||||
bash("arch=$(uname -i)");
|
||||
|
||||
println("Your OS: " ++ os);
|
||||
println("Distribution: " ++ distribution);
|
||||
println("Arch: " ++ arch);
|
||||
|
||||
if ("$3" != "") {
|
||||
port = "$3";
|
||||
} else {
|
||||
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
|
||||
|
||||
if (port == "") {
|
||||
port = defaultPort;
|
||||
}
|
||||
}
|
||||
|
||||
if ("$2" != "") {
|
||||
installPath = "$2";
|
||||
} else {
|
||||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
|
||||
|
||||
if (installPath == "") {
|
||||
installPath = defaultInstallPath;
|
||||
}
|
||||
}
|
||||
|
||||
// CentOS
|
||||
if (distribution == "rhel") {
|
||||
bash("nodeCheck=$(node -v)");
|
||||
|
||||
if (nodeCheck != "") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("yum -y -q install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("yum install -y -q nodejs");
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
|
||||
if (nodeCheckAgain == "") {
|
||||
println("Error during Node.js installation");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
println("Installing Git");
|
||||
bash("yum -y -q install git");
|
||||
}
|
||||
|
||||
// Ubuntu
|
||||
} else if (distribution == "ubuntu") {
|
||||
deb();
|
||||
|
||||
// Debian
|
||||
} else if (distribution == "debian") {
|
||||
deb();
|
||||
|
||||
} else {
|
||||
// Unknown distribution
|
||||
error = 0;
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: git is missing");
|
||||
}
|
||||
|
||||
bash("check=$(node -v)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: node is missing");
|
||||
}
|
||||
|
||||
if (error > 0) {
|
||||
println("Please install above missing software");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Installing PM2");
|
||||
bash("npm install pm2 -g");
|
||||
bash("pm2 startup");
|
||||
}
|
||||
|
||||
bash("mkdir -p $installPath");
|
||||
bash("cd $installPath");
|
||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||
bash("npm run setup");
|
||||
|
||||
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port");
|
||||
|
||||
} else {
|
||||
defaultVolume = "uptime-kuma";
|
||||
|
||||
bash("check=$(docker -v)");
|
||||
if (check == "") {
|
||||
println("Error: docker is not found!");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("check=$(docker info)");
|
||||
|
||||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
|
||||
\"echo\" \"Error: docker is not running\"
|
||||
\"exit\" \"1\"
|
||||
fi");
|
||||
|
||||
if ("$3" != "") {
|
||||
port = "$3";
|
||||
} else {
|
||||
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
|
||||
|
||||
if (port == "") {
|
||||
port = defaultPort;
|
||||
}
|
||||
}
|
||||
|
||||
if ("$2" != "") {
|
||||
volume = "$2";
|
||||
} else {
|
||||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
|
||||
|
||||
if (volume == "") {
|
||||
volume = defaultVolume;
|
||||
}
|
||||
}
|
||||
|
||||
println("Port: $port");
|
||||
println("Volume: $volume");
|
||||
bash("docker volume create $volume");
|
||||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
|
||||
}
|
||||
|
||||
println("http://localhost:$port");
|
24
extra/mark-as-nightly.js
Normal file
24
extra/mark-as-nightly.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const pkg = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const util = require("../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
59
extra/reset-password.js
Normal file
59
extra/reset-password.js
Normal file
@@ -0,0 +1,59 @@
|
||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
||||
|
||||
console.log("Loading the database");
|
||||
|
||||
const Database = require("../server/database");
|
||||
const { R } = require("redbean-node");
|
||||
const readline = require("readline");
|
||||
const { initJWTSecret } = require("../server/util-server");
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await Database.connect();
|
||||
|
||||
try {
|
||||
const user = await R.findOne("user");
|
||||
|
||||
if (! user) {
|
||||
throw new Error("user not found, have you installed?");
|
||||
}
|
||||
|
||||
console.log("Found user: " + user.username);
|
||||
|
||||
while (true) {
|
||||
let password = await question("New Password: ");
|
||||
let confirmPassword = await question("Confirm New Password: ");
|
||||
|
||||
if (password === confirmPassword) {
|
||||
await user.resetPassword(password);
|
||||
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
rl.close();
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Password reset successfully.");
|
||||
} catch (e) {
|
||||
console.error("Error: " + e.message);
|
||||
}
|
||||
|
||||
await Database.close();
|
||||
|
||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
||||
})();
|
||||
|
||||
function question(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer);
|
||||
})
|
||||
});
|
||||
}
|
144
extra/simple-dns-server.js
Normal file
144
extra/simple-dns-server.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Simple DNS Server
|
||||
* For testing DNS monitoring type, dev only
|
||||
*/
|
||||
const dns2 = require("dns2");
|
||||
|
||||
const { Packet } = dns2;
|
||||
|
||||
const server = dns2.createServer({
|
||||
udp: true
|
||||
});
|
||||
|
||||
server.on("request", (request, send, rinfo) => {
|
||||
for (let question of request.questions) {
|
||||
console.log(question.name, type(question.type), question.class);
|
||||
|
||||
const response = Packet.createResponseFromRequest(request);
|
||||
|
||||
if (question.name === "existing.com") {
|
||||
|
||||
if (question.type === Packet.TYPE.A) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
address: "1.2.3.4"
|
||||
});
|
||||
} if (question.type === Packet.TYPE.AAAA) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
address: "fe80::::1234:5678:abcd:ef00",
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.CNAME) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
domain: "cname1.existing.com",
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.MX) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
exchange: "mx1.existing.com",
|
||||
priority: 5
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.NS) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
ns: "ns1.existing.com",
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.SOA) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
primary: "existing.com",
|
||||
admin: "admin@existing.com",
|
||||
serial: 2021082701,
|
||||
refresh: 300,
|
||||
retry: 3,
|
||||
expiration: 10,
|
||||
minimum: 10,
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.SRV) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
priority: 5,
|
||||
weight: 5,
|
||||
port: 8080,
|
||||
target: "srv1.existing.com",
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.TXT) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
data: "#v=spf1 include:_spf.existing.com ~all",
|
||||
});
|
||||
} else if (question.type === Packet.TYPE.CAA) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
flags: 0,
|
||||
tag: "issue",
|
||||
value: "ca.existing.com",
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (question.name === "4.3.2.1.in-addr.arpa") {
|
||||
if (question.type === Packet.TYPE.PTR) {
|
||||
response.answers.push({
|
||||
name: question.name,
|
||||
type: question.type,
|
||||
class: question.class,
|
||||
ttl: 300,
|
||||
domain: "ptr1.existing.com",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
send(response);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("listening", () => {
|
||||
console.log("Listening");
|
||||
console.log(server.addresses());
|
||||
});
|
||||
|
||||
server.on("close", () => {
|
||||
console.log("server closed");
|
||||
});
|
||||
|
||||
server.listen({
|
||||
udp: 5300
|
||||
});
|
||||
|
||||
function type(code) {
|
||||
for (let name in Packet.TYPE) {
|
||||
if (Packet.TYPE[name] === code) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
3
extra/update-language-files/.gitignore
vendored
Normal file
3
extra/update-language-files/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
package-lock.json
|
||||
test.js
|
||||
languages/
|
78
extra/update-language-files/index.js
Normal file
78
extra/update-language-files/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// Need to use es6 to read language files
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
|
||||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
||||
/**
|
||||
* Look ma, it's cp -R.
|
||||
* @param {string} src The path to the thing to copy.
|
||||
* @param {string} dest The path to the new copy.
|
||||
*/
|
||||
const copyRecursiveSync = function (src, dest) {
|
||||
let exists = fs.existsSync(src);
|
||||
let stats = exists && fs.statSync(src);
|
||||
let isDirectory = exists && stats.isDirectory();
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(dest);
|
||||
fs.readdirSync(src).forEach(function (childItemName) {
|
||||
copyRecursiveSync(path.join(src, childItemName),
|
||||
path.join(dest, childItemName));
|
||||
});
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
console.log(process.argv)
|
||||
const baseLangCode = process.argv[2] || "zh-HK";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
|
||||
const en = (await import("./languages/en.js")).default;
|
||||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
||||
const files = fs.readdirSync("./languages");
|
||||
console.log(files);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".js")) {
|
||||
console.log("Processing " + file);
|
||||
const lang = await import("./languages/" + file);
|
||||
|
||||
let obj;
|
||||
|
||||
if (lang.default) {
|
||||
console.log("is js module");
|
||||
obj = lang.default;
|
||||
} else {
|
||||
console.log("empty file");
|
||||
obj = {
|
||||
languageName: "<Your Language name in your language (not in English)>"
|
||||
};
|
||||
}
|
||||
|
||||
// En first
|
||||
for (const key in en) {
|
||||
if (! obj[key]) {
|
||||
obj[key] = en[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Base second
|
||||
for (const key in baseLang) {
|
||||
if (! obj[key]) {
|
||||
obj[key] = key;
|
||||
}
|
||||
}
|
||||
|
||||
const code = "export default " + util.inspect(obj, {
|
||||
depth: null,
|
||||
});
|
||||
|
||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
console.log("Done, fix the format by eslint now");
|
12
extra/update-language-files/package.json
Normal file
12
extra/update-language-files/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "update-language-files",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
61
extra/update-version.js
Normal file
61
extra/update-version.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const pkg = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const child_process = require("child_process");
|
||||
const util = require("../src/util");
|
||||
|
||||
util.polyfill();
|
||||
|
||||
const oldVersion = pkg.version;
|
||||
const newVersion = process.argv[2];
|
||||
|
||||
console.log("Old Version: " + oldVersion);
|
||||
console.log("New Version: " + newVersion);
|
||||
|
||||
if (! newVersion) {
|
||||
console.error("invalid version");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const exists = tagExists(newVersion);
|
||||
|
||||
if (! exists) {
|
||||
// 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);
|
||||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
|
||||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
} else {
|
||||
console.log("version exists")
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
let msg = "update to " + version;
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout)
|
||||
|
||||
if (stdout.includes("no changes added to commit")) {
|
||||
throw new Error("commit error")
|
||||
}
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
console.log(res.stdout.toString().trim())
|
||||
}
|
||||
|
||||
function tagExists(version) {
|
||||
if (! version) {
|
||||
throw new Error("invalid version");
|
||||
}
|
||||
|
||||
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
@@ -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))
|
||||
}
|
||||
|
17
index.html
17
index.html
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="theme-color" id="theme-color" content="" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
203
install.sh
Normal file
203
install.sh
Normal file
@@ -0,0 +1,203 @@
|
||||
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
||||
# "npm run compile-install-script" to compile install.sh
|
||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Uptime Kuma Installer"
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" ""
|
||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||
"echo" "-e" ""
|
||||
if [ "$1" != "" ]; then
|
||||
type="$1"
|
||||
else
|
||||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
|
||||
fi
|
||||
defaultPort="3001"
|
||||
function checkNode {
|
||||
local _0
|
||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||
_0="12"
|
||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
||||
"echo" "-e" "Error: Required Node.js 14"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$nodeVersion" == "12" ]; then
|
||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
||||
fi
|
||||
}
|
||||
function deb {
|
||||
nodeCheck=$(node -v)
|
||||
apt --yes update
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
# Old nodejs binary name is "nodejs"
|
||||
check=$(nodejs --version)
|
||||
if [ "$check" != "" ]; then
|
||||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
|
||||
exit 1
|
||||
fi
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
apt --yes install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
||||
apt --yes install nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
"echo" "-e" "Error during Node.js installation"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing Git"
|
||||
apt --yes install git
|
||||
fi
|
||||
}
|
||||
if [ "$type" == "local" ]; then
|
||||
defaultInstallPath="/opt/uptime-kuma"
|
||||
if [ -e "/etc/redhat-release" ]; then
|
||||
os=$("cat" "/etc/redhat-release")
|
||||
distribution="rhel"
|
||||
else
|
||||
if [ -e "/etc/issue" ]; then
|
||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||
if [ "$os" == "Ubuntu" ]; then
|
||||
distribution="ubuntu"
|
||||
fi
|
||||
if [ "$os" == "Debian" ]; then
|
||||
distribution="debian"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
arch=$(uname -i)
|
||||
"echo" "-e" "Your OS: ""$os"
|
||||
"echo" "-e" "Distribution: ""$distribution"
|
||||
"echo" "-e" "Arch: ""$arch"
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
else
|
||||
"read" "-p" "Listening Port [$defaultPort]: " "port"
|
||||
if [ "$port" == "" ]; then
|
||||
port="$defaultPort"
|
||||
fi
|
||||
fi
|
||||
if [ "$2" != "" ]; then
|
||||
installPath="$2"
|
||||
else
|
||||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
|
||||
if [ "$installPath" == "" ]; then
|
||||
installPath="$defaultInstallPath"
|
||||
fi
|
||||
fi
|
||||
# CentOS
|
||||
if [ "$distribution" == "rhel" ]; then
|
||||
nodeCheck=$(node -v)
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
||||
yum install -y -q nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
"echo" "-e" "Error during Node.js installation"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing Git"
|
||||
yum -y -q install git
|
||||
fi
|
||||
# Ubuntu
|
||||
else
|
||||
if [ "$distribution" == "ubuntu" ]; then
|
||||
"deb"
|
||||
# Debian
|
||||
else
|
||||
if [ "$distribution" == "debian" ]; then
|
||||
"deb"
|
||||
else
|
||||
# Unknown distribution
|
||||
error=$((0))
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: git is missing"
|
||||
fi
|
||||
check=$(node -v)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: node is missing"
|
||||
fi
|
||||
if [ $(($error > 0)) == 1 ]; then
|
||||
"echo" "-e" "Please install above missing software"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g
|
||||
pm2 startup
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
cd $installPath
|
||||
git clone https://github.com/louislam/uptime-kuma.git .
|
||||
npm run setup
|
||||
pm2 start server/server.js --name uptime-kuma -- --port=$port
|
||||
else
|
||||
defaultVolume="uptime-kuma"
|
||||
check=$(docker -v)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Error: docker is not found!"
|
||||
exit 1
|
||||
fi
|
||||
check=$(docker info)
|
||||
if [[ "$check" == *"Is the docker daemon running"* ]]; then
|
||||
"echo" "Error: docker is not running"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
else
|
||||
"read" "-p" "Expose Port [$defaultPort]: " "port"
|
||||
if [ "$port" == "" ]; then
|
||||
port="$defaultPort"
|
||||
fi
|
||||
fi
|
||||
if [ "$2" != "" ]; then
|
||||
volume="$2"
|
||||
else
|
||||
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
|
||||
if [ "$volume" == "" ]; then
|
||||
volume="$defaultVolume"
|
||||
fi
|
||||
fi
|
||||
"echo" "-e" "Port: $port"
|
||||
"echo" "-e" "Volume: $volume"
|
||||
docker volume create $volume
|
||||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
fi
|
||||
"echo" "-e" "http://localhost:$port"
|
28
kubernetes/README.md
Normal file
28
kubernetes/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Uptime-Kuma K8s Deployment
|
||||
## How does it work?
|
||||
|
||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
||||
|
||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
|
||||
|
||||
## What do i have to edit?
|
||||
You have to edit the ```ingressroute.yml``` to your needs.
|
||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
||||
|
||||
- host
|
||||
- secrets and secret names
|
||||
- (Cluster)Issuer (optional)
|
||||
- the Version in the Deployment-File
|
||||
- update:
|
||||
- change to newer version and run the above commands, it will update the pods one after another
|
||||
|
||||
## How To use:
|
||||
|
||||
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
||||
- Edit files mentioned above to your needs
|
||||
- run ```kustomize build > apply.yml```
|
||||
- run ```kubectl apply -f apply.yml```
|
||||
|
||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
10
kubernetes/kustomization.yml
Normal file
10
kubernetes/kustomization.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace: uptime-kuma
|
||||
namePrefix: uptime-kuma-
|
||||
|
||||
commonLabels:
|
||||
app: uptime-kuma
|
||||
|
||||
bases:
|
||||
- uptime-kuma
|
||||
|
||||
|
42
kubernetes/uptime-kuma/deployment.yml
Normal file
42
kubernetes/uptime-kuma/deployment.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
component: uptime-kuma
|
||||
name: deployment
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
component: uptime-kuma
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: uptime-kuma
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: louislam/uptime-kuma:1
|
||||
ports:
|
||||
- containerPort: 3001
|
||||
volumeMounts:
|
||||
- mountPath: /app/data
|
||||
name: storage
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- node
|
||||
- extra/healthcheck.js
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3001
|
||||
scheme: HTTP
|
||||
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: pvc
|
39
kubernetes/uptime-kuma/ingressroute.yml
Normal file
39
kubernetes/uptime-kuma/ingressroute.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/server-snippets: |
|
||||
location / {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
name: ingress
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- example.com
|
||||
secretName: example-com-tls
|
||||
rules:
|
||||
- host: example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
number: 3001
|
5
kubernetes/uptime-kuma/kustomization.yml
Normal file
5
kubernetes/uptime-kuma/kustomization.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
resources:
|
||||
- deployment.yml
|
||||
- service.yml
|
||||
- ingressroute.yml
|
||||
- pvc.yml
|
10
kubernetes/uptime-kuma/pvc.yml
Normal file
10
kubernetes/uptime-kuma/pvc.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 4Gi
|
13
kubernetes/uptime-kuma/service.yml
Normal file
13
kubernetes/uptime-kuma/service.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: service
|
||||
spec:
|
||||
selector:
|
||||
component: uptime-kuma
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
protocol: TCP
|
16958
package-lock.json
generated
16958
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@@ -1,45 +1,92 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.0.4",
|
||||
"version": "1.5.3",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.*"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"update": "",
|
||||
"build": "vite build",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.4 . --push",
|
||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly . --push",
|
||||
"setup": "git checkout 1.0.4 && npm install && npm run build",
|
||||
"version-global-replace": "node extra/version-global-replace.js"
|
||||
"build-docker": "npm run build-docker-alpine && npm run build-docker-debian",
|
||||
"build-docker-alpine": "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.5.3 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push",
|
||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --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 --progress plain",
|
||||
"setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bootstrap": "^5.0.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.1.0",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "^3.6.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.2",
|
||||
"nodemailer": "^6.6.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"redbean-node": "0.0.20",
|
||||
"socket.io": "^4.0.2",
|
||||
"socket.io-client": "^4.1.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"prom-client": "^13.2.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"redbean-node": "0.1.2",
|
||||
"socket.io": "^4.2.0",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"vue": "^3.0.5",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"vue": "^3.2.8",
|
||||
"vue-chart-3": "^0.5.7",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-router": "^4.0.10",
|
||||
"vue-i18n": "^9.1.7",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-toastification": "^2.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-legacy": "^1.4.3",
|
||||
"@vitejs/plugin-vue": "^1.2.3",
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"core-js": "^3.15.2",
|
||||
"sass": "^1.35.1",
|
||||
"vite": "^2.3.7"
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@types/bootstrap": "^5.1.2",
|
||||
"@vitejs/plugin-legacy": "^1.5.2",
|
||||
"@vitejs/plugin-vue": "^1.6.0",
|
||||
"@vue/compiler-sfc": "^3.2.6",
|
||||
"core-js": "^3.17.0",
|
||||
"dns2": "^2.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"sass": "^1.38.2",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.4.2",
|
||||
"vite": "^2.5.3"
|
||||
}
|
||||
}
|
||||
|
BIN
public/apple-touch-icon-precomposed.png
Normal file
BIN
public/apple-touch-icon-precomposed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
51
server/auth.js
Normal file
51
server/auth.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const basicAuth = require("express-basic-auth")
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { debug } = require("../src/util");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param username : string
|
||||
* @param password : string
|
||||
* @returns {Promise<Bean|null>}
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
username,
|
||||
])
|
||||
|
||||
if (user && passwordHash.verify(password, user.password)) {
|
||||
// Upgrade the hash to bcrypt
|
||||
if (passwordHash.needRehash(user.password)) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(password),
|
||||
user.id,
|
||||
]);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function myAuthorizer(username, password, callback) {
|
||||
|
||||
setting("disableAuth").then((result) => {
|
||||
|
||||
if (result) {
|
||||
callback(null, true)
|
||||
} else {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
exports.basicAuth = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
44
server/check-version.js
Normal file
44
server/check-version.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { setSetting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const { isDev } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
|
||||
let interval;
|
||||
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
try {
|
||||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
|
||||
|
||||
if (typeof res.data === "string") {
|
||||
res.data = JSON.parse(res.data);
|
||||
}
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.version = "1000.0.0"
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
console.log("Latest Version: " + exports.latestVersion);
|
||||
} catch (_) { }
|
||||
|
||||
};
|
||||
|
||||
check();
|
||||
interval = setInterval(check, 3600 * 1000 * 48);
|
||||
};
|
||||
|
||||
exports.enableCheckUpdate = async (value) => {
|
||||
await setSetting("checkUpdate", value);
|
||||
|
||||
clearInterval(interval);
|
||||
|
||||
if (value) {
|
||||
exports.startInterval();
|
||||
}
|
||||
};
|
||||
|
||||
exports.socket = null;
|
146
server/database.js
Normal file
146
server/database.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const fs = require("fs");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
|
||||
class Database {
|
||||
|
||||
static templatePath = "./db/kuma.db"
|
||||
static dataDir;
|
||||
static path;
|
||||
static latestVersion = 8;
|
||||
static noReject = true;
|
||||
static sqliteInstance = null;
|
||||
|
||||
static async connect() {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
R.setup("sqlite", {
|
||||
filename: Database.path,
|
||||
useNullAsDefault: true,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
}, {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
});
|
||||
|
||||
if (process.env.SQL_LOG === "1") {
|
||||
R.debug(true);
|
||||
}
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
}
|
||||
|
||||
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 if (version > this.latestVersion) {
|
||||
console.info("Warning: Database version is newer than expected");
|
||||
} else {
|
||||
console.info("Database patch is needed")
|
||||
|
||||
console.info("Backup the db")
|
||||
const backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.copyFileSync(walPath, walPath + ".bak" + version);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
static getBetterSQLite3Database() {
|
||||
return R.knex.client.acquireConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async close() {
|
||||
if (this.sqliteInstance) {
|
||||
this.sqliteInstance.close();
|
||||
}
|
||||
console.log("Stopped database");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
@@ -1,17 +1,15 @@
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
var timezone = require('dayjs/plugin/timezone')
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const {R} = require("redbean-node");
|
||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
||||
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
/**
|
||||
* status:
|
||||
* 0 = DOWN
|
||||
* 1 = UP
|
||||
* 2 = PENDING
|
||||
*/
|
||||
class Heartbeat extends BeanModel {
|
||||
|
||||
|
@@ -1,28 +1,31 @@
|
||||
|
||||
const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
var timezone = require('dayjs/plugin/timezone')
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const {tcping, ping} = require("../util-server");
|
||||
const {R} = require("redbean-node");
|
||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
||||
const {Notification} = require("../notification")
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification")
|
||||
const version = require("../../package.json").version;
|
||||
|
||||
/**
|
||||
* status:
|
||||
* 0 = DOWN
|
||||
* 1 = UP
|
||||
* 2 = PENDING
|
||||
*/
|
||||
class Monitor extends BeanModel {
|
||||
|
||||
async toJSON() {
|
||||
|
||||
let notificationIDList = {};
|
||||
|
||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||
this.id
|
||||
this.id,
|
||||
])
|
||||
|
||||
for (let bean of list) {
|
||||
@@ -35,50 +38,118 @@ class Monitor extends BeanModel {
|
||||
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
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
maxredirects: this.maxredirects,
|
||||
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
notificationIDList,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIgnoreTls() {
|
||||
return Boolean(this.ignoreTls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isUpsideDown() {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
getAcceptedStatuscodes() {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
let prometheus = new Prometheus(this);
|
||||
|
||||
const beat = async () => {
|
||||
console.log(`Monitor ${this.id}: Heartbeat`)
|
||||
|
||||
// Expose here for prometheus update
|
||||
// undefined if not https
|
||||
let tlsInfo = undefined;
|
||||
|
||||
if (! previousBeat) {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
this.id
|
||||
this.id,
|
||||
])
|
||||
}
|
||||
|
||||
const isFirstBeat = !previousBeat;
|
||||
|
||||
let bean = R.dispense("heartbeat")
|
||||
bean.monitor_id = this.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = 0;
|
||||
bean.status = DOWN;
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
bean.status = flipStatus(bean.status);
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (previousBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
|
||||
if (! isFirstBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||
} else {
|
||||
bean.duration = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.type === "http" || this.type === "keyword") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
let res = await axios.get(this.url, {
|
||||
headers: { 'User-Agent':'Uptime-Kuma' }
|
||||
})
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
}),
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
});
|
||||
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 {
|
||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
} else {
|
||||
|
||||
let data = res.data;
|
||||
@@ -90,43 +161,117 @@ class Monitor extends BeanModel {
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found"
|
||||
bean.status = 1;
|
||||
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 = 1;
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.msg = ""
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
} else if (this.type === "dns") {
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsMessage = "";
|
||||
|
||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
|
||||
dnsMessage += "Records: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
|
||||
dnsMessage = dnsRes[0];
|
||||
} else if (this.dns_resolve_type == "CAA") {
|
||||
dnsMessage = dnsRes[0].issue;
|
||||
} else if (this.dns_resolve_type == "MX") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2)
|
||||
} else if (this.dns_resolve_type == "NS") {
|
||||
dnsMessage += "Servers: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type == "SOA") {
|
||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
} else if (this.dns_resolve_type == "SRV") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2)
|
||||
}
|
||||
|
||||
if (this.dnsLastResult !== dnsMessage) {
|
||||
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
|
||||
dnsMessage,
|
||||
this.id
|
||||
]);
|
||||
}
|
||||
|
||||
bean.msg = dnsMessage;
|
||||
bean.status = UP;
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
bean.status = flipStatus(bean.status);
|
||||
|
||||
if (bean.status === DOWN) {
|
||||
throw new Error("Flip UP to DOWN");
|
||||
}
|
||||
}
|
||||
|
||||
retries = 0;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
bean.msg = error.message;
|
||||
|
||||
// If UP come in here, it must be upside down mode
|
||||
// Just reset the retries
|
||||
if (this.isUpsideDown() && bean.status === UP) {
|
||||
retries = 0;
|
||||
|
||||
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as important if status changed
|
||||
if (! previousBeat || previousBeat.status !== bean.status) {
|
||||
// * ? -> 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;
|
||||
|
||||
// Do not send if first beat is UP
|
||||
if (previousBeat || bean.status !== 1) {
|
||||
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [
|
||||
this.id
|
||||
// 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 promiseList = [];
|
||||
|
||||
let text;
|
||||
if (bean.status === 1) {
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up"
|
||||
} else {
|
||||
text = "🔴 Down"
|
||||
@@ -134,37 +279,90 @@ class Monitor extends BeanModel {
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for(let notification of notificationList) {
|
||||
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
|
||||
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);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
||||
} else if (bean.status === PENDING) {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
|
||||
} else {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||
}
|
||||
|
||||
await R.store(bean)
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
Monitor.sendStats(io, this.id, this.user_id)
|
||||
|
||||
await R.store(bean);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
}
|
||||
|
||||
beat();
|
||||
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method:
|
||||
* returns URL object for further usage
|
||||
* returns null if url is invalid
|
||||
* @returns {null|URL}
|
||||
*/
|
||||
getUrl() {
|
||||
try {
|
||||
return new URL(this.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
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);
|
||||
|
||||
return checkCertificateResult;
|
||||
}
|
||||
|
||||
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);
|
||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||
|
||||
if (hasClients) {
|
||||
await Monitor.sendAvgPing(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||
} else {
|
||||
debug("No clients in the room, no need to send stats");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,6 +370,8 @@ class Monitor extends BeanModel {
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let avgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping)
|
||||
FROM heartbeat
|
||||
@@ -179,12 +379,23 @@ class Monitor extends BeanModel {
|
||||
AND ping IS NOT NULL
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
monitorID
|
||||
monitorID,
|
||||
]));
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
|
||||
|
||||
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:
|
||||
@@ -192,6 +403,8 @@ class Monitor extends BeanModel {
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let sec = duration * 3600;
|
||||
|
||||
let heartbeatList = await R.getAll(`
|
||||
@@ -200,9 +413,11 @@ class Monitor extends BeanModel {
|
||||
WHERE time > DATETIME('now', ? || ' hours')
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
monitorID
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
||||
|
||||
let downtime = 0;
|
||||
let total = 0;
|
||||
let uptime;
|
||||
@@ -224,7 +439,7 @@ class Monitor extends BeanModel {
|
||||
// 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');
|
||||
let trim = dayjs.utc().diff(dayjs(time), "second");
|
||||
value = sec - trim;
|
||||
|
||||
if (value < 0) {
|
||||
@@ -233,7 +448,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
total += value;
|
||||
if (row.status === 0) {
|
||||
if (row.status === 0 || row.status === 2) {
|
||||
downtime += value;
|
||||
}
|
||||
}
|
||||
@@ -245,8 +460,6 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
}
|
||||
|
21
server/model/user.js
Normal file
21
server/model/user.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const passwordHash = require("../password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class User extends BeanModel {
|
||||
|
||||
/**
|
||||
* Direct execute, no need R.store()
|
||||
* @param newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
passwordHash.generate(newPassword),
|
||||
this.id
|
||||
]);
|
||||
this.password = newPassword;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
@@ -1,43 +1,57 @@
|
||||
const axios = require("axios");
|
||||
const {R} = require("redbean-node");
|
||||
const FormData = require('form-data');
|
||||
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 true;
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
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.endsWith("/")) {
|
||||
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": 8,
|
||||
"title": "Uptime-Kuma"
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
})
|
||||
return true;
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "webhook") {
|
||||
try {
|
||||
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
@@ -48,21 +62,21 @@ class Notification {
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append('data', JSON.stringify(data));
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
|
||||
config = {
|
||||
headers: finalData.getHeaders()
|
||||
headers: finalData.getHeaders(),
|
||||
}
|
||||
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
let res = await axios.post(notification.webhookURL, finalData, config)
|
||||
return true;
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "smtp") {
|
||||
@@ -70,61 +84,515 @@ class Notification {
|
||||
|
||||
} else if (notification.type === "discord") {
|
||||
try {
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if(heartbeatJSON == null) {
|
||||
let data = {
|
||||
username: 'Uptime-Kuma',
|
||||
content: msg
|
||||
}
|
||||
let res = await axios.post(notification.discordWebhookUrl, data)
|
||||
return true;
|
||||
}
|
||||
// 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
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let discordtestdata = {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
let res = await axios.post(notification.discordWebhookUrl, data)
|
||||
return true;
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let discorddowndata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
|
||||
color: 16711680,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let discordupdata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
|
||||
color: 65280,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||
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 = {};
|
||||
try {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"number": notification.signalNumber,
|
||||
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
|
||||
};
|
||||
let config = {};
|
||||
|
||||
let res = await axios.post(notification.signalURL, data, config)
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushy") {
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
"to": notification.pushyToken,
|
||||
"data": {
|
||||
"message": "Uptime-Kuma"
|
||||
},
|
||||
"notification": {
|
||||
"body": msg,
|
||||
"badge": 1,
|
||||
"sound": "ping.aiff"
|
||||
}
|
||||
})
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
} else if (notification.type === "octopush") {
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [
|
||||
{
|
||||
"phone_number": notification.octopushPhoneNumber
|
||||
}
|
||||
],
|
||||
//octopush not supporting non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": notification.octopushSMSType,
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
} 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 === "rocket.chat") {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Rocket.chat testing successful.",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title_link": notification.rocketbutton,
|
||||
"text": "*Message*\n" + msg,
|
||||
"color": "#32cd32"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "mattermost") {
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
}
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel;
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushover") {
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "apprise") {
|
||||
|
||||
return Notification.apprise(notification, msg)
|
||||
|
||||
} else if (notification.type === "lunasea") {
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(lunaseadevice, testdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, downdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 1) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, updata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushbullet") {
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"type": "note",
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(pushbulletUrl, testdata, config)
|
||||
} else if (heartbeatJSON["status"] == 0) {
|
||||
let downdata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, downdata, config)
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let updata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, updata, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
} else if (notification.type === "line") {
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.lineChannelAccessToken
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, testMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 0) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, downMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, upMessage, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
} else {
|
||||
throw new Error("Notification type is not supported")
|
||||
}
|
||||
@@ -168,38 +636,70 @@ class Notification {
|
||||
|
||||
static async smtp(notification, msg) {
|
||||
|
||||
let transporter = nodemailer.createTransport({
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
auth: {
|
||||
};
|
||||
|
||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||
if (notification.smtpUsername || notification.smtpPassword) {
|
||||
config.auth = {
|
||||
user: notification.smtpUsername,
|
||||
pass: notification.smtpPassword,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
// send mail with defined transport object
|
||||
let info = await transporter.sendMail({
|
||||
await transporter.sendMail({
|
||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
text: msg,
|
||||
});
|
||||
|
||||
return true;
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
|
||||
static async discord(notification, msg) {
|
||||
const client = new Discord.Client();
|
||||
await client.login(notification.discordToken)
|
||||
static async apprise(notification, msg) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
|
||||
const channel = await client.channels.fetch(notification.discordChannelID);
|
||||
await channel.send(msg);
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
client.destroy()
|
||||
if (output) {
|
||||
|
||||
return true;
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
}
|
||||
|
||||
throw new Error(output)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require("command-exists").sync;
|
||||
let exists = commandExistsSync("apprise");
|
||||
return exists;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const passwordHashOld = require('password-hash');
|
||||
const bcrypt = require('bcrypt');
|
||||
const passwordHashOld = require("password-hash");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const saltRounds = 10;
|
||||
|
||||
exports.generate = function (password) {
|
||||
@@ -9,9 +9,9 @@ exports.generate = function (password) {
|
||||
exports.verify = function (password, hash) {
|
||||
if (isSHA1(hash)) {
|
||||
return passwordHashOld.verify(password, hash)
|
||||
} else {
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
|
||||
function isSHA1(hash) {
|
||||
|
@@ -1,45 +1,75 @@
|
||||
// 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);
|
||||
const net = require("net");
|
||||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const WIN = /^win/.test(process.platform);
|
||||
const LIN = /^linux/.test(process.platform);
|
||||
const MAC = /^darwin/.test(process.platform);
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
function Ping(host, options) {
|
||||
if (!host)
|
||||
throw new Error('You must specify a host to ping!');
|
||||
if (!host) {
|
||||
throw new Error("You must specify a host to ping!");
|
||||
}
|
||||
|
||||
this._host = host;
|
||||
this._options = options = (options || {});
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
const timeout = 10;
|
||||
|
||||
if (WIN) {
|
||||
this._bin = 'c:/windows/system32/ping.exe';
|
||||
this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ];
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, 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 ];
|
||||
|
||||
} else if (LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
defaultArgs.unshift("-6");
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
}
|
||||
else {
|
||||
throw new Error('Could not detect your ping binary.');
|
||||
|
||||
} else if (MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
this._bin = "/sbin/ping6";
|
||||
} else {
|
||||
this._bin = "/sbin/ping";
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
defaultArgs.unshift("-6");
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
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');
|
||||
if (!fs.existsSync(this._bin)) {
|
||||
throw new Error("Could not detect " + this._bin + " on your system");
|
||||
}
|
||||
|
||||
this._i = 0;
|
||||
|
||||
@@ -50,62 +80,73 @@ 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);
|
||||
Ping.prototype.send = function (callback) {
|
||||
let self = this;
|
||||
callback = callback || function (err, ms) {
|
||||
if (err) {
|
||||
return self.emit("error", err);
|
||||
}
|
||||
return self.emit("result", ms);
|
||||
};
|
||||
|
||||
var _ended, _exited, _errored;
|
||||
let _ended;
|
||||
let _exited;
|
||||
let _errored;
|
||||
|
||||
this._ping = spawn(this._bin, this._args); // spawn the binary
|
||||
|
||||
this._ping.on('error', function(err) { // handle binary errors
|
||||
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("data", function (data) { // log stdout
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
this._ping.stdout.on('end', function() {
|
||||
this._ping.stdout.on("end", function () {
|
||||
_ended = true;
|
||||
if (_exited && !_errored) onEnd.call(self._ping);
|
||||
if (_exited && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
this._ping.stderr.on('data', function(data) { // log stderr
|
||||
this._stderr = (this._stderr || '') + data;
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
this._ping.on('exit', function(code) { // handle complete
|
||||
this._ping.on("exit", function (code) { // handle complete
|
||||
_exited = true;
|
||||
if (_ended && !_errored) onEnd.call(self._ping);
|
||||
if (_ended && !_errored) {
|
||||
onEnd.call(self._ping);
|
||||
}
|
||||
});
|
||||
|
||||
function onEnd() {
|
||||
var stdout = this.stdout._stdout,
|
||||
stderr = this.stderr._stderr,
|
||||
ms;
|
||||
let stdout = this.stdout._stdout;
|
||||
let stderr = this.stderr._stderr;
|
||||
let ms;
|
||||
|
||||
if (stderr)
|
||||
if (stderr) {
|
||||
return callback(new Error(stderr));
|
||||
else if (!stdout)
|
||||
return callback(new Error('No stdout detected'));
|
||||
}
|
||||
|
||||
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);
|
||||
callback(null, ms, stdout);
|
||||
}
|
||||
};
|
||||
|
||||
// CALL Ping#send(callback) ON A TIMER
|
||||
// ===================================
|
||||
Ping.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
this._i = setInterval(function() {
|
||||
Ping.prototype.start = function (callback) {
|
||||
let self = this;
|
||||
this._i = setInterval(function () {
|
||||
self.send(callback);
|
||||
}, (self._options.interval || 5000));
|
||||
self.send(callback);
|
||||
@@ -113,6 +154,6 @@ Ping.prototype.start = function(callback) {
|
||||
|
||||
// STOP SENDING PINGS
|
||||
// ==================
|
||||
Ping.prototype.stop = function() {
|
||||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
|
90
server/prometheus.js
Normal file
90
server/prometheus.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const PrometheusClient = require("prom-client");
|
||||
|
||||
const commonLabels = [
|
||||
"monitor_name",
|
||||
"monitor_type",
|
||||
"monitor_url",
|
||||
"monitor_hostname",
|
||||
"monitor_port",
|
||||
]
|
||||
|
||||
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_days_remaining",
|
||||
help: "The number of days remaining until the certificate expires",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
const monitor_cert_is_valid = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_is_valid",
|
||||
help: "Is the certificate still valid? (1 = Yes, 0= No)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
const monitor_response_time = new PrometheusClient.Gauge({
|
||||
name: "monitor_response_time",
|
||||
help: "Monitor Response Time (ms)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
const monitor_status = new PrometheusClient.Gauge({
|
||||
name: "monitor_status",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
class Prometheus {
|
||||
monitorLabelValues = {}
|
||||
|
||||
constructor(monitor) {
|
||||
this.monitorLabelValues = {
|
||||
monitor_name: monitor.name,
|
||||
monitor_type: monitor.type,
|
||||
monitor_url: monitor.url,
|
||||
monitor_hostname: monitor.hostname,
|
||||
monitor_port: monitor.port
|
||||
}
|
||||
}
|
||||
|
||||
update(heartbeat, tlsInfo) {
|
||||
if (typeof tlsInfo !== "undefined") {
|
||||
try {
|
||||
let is_valid = 0
|
||||
if (tlsInfo.valid == true) {
|
||||
is_valid = 1
|
||||
} else {
|
||||
is_valid = 0
|
||||
}
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
|
||||
} else {
|
||||
// Is it good?
|
||||
monitor_response_time.set(this.monitorLabelValues, -1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Prometheus
|
||||
}
|
455
server/server.js
455
server/server.js
@@ -1,46 +1,157 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const http = require('http');
|
||||
const server = http.createServer(app);
|
||||
const { Server } = require("socket.io");
|
||||
const io = new Server(server);
|
||||
const dayjs = require("dayjs");
|
||||
const {R} = require("redbean-node");
|
||||
const passwordHash = require('./password-hash');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Monitor = require("./model/monitor");
|
||||
console.log("Welcome to Uptime Kuma");
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
|
||||
|
||||
console.log("Importing Node libraries")
|
||||
const fs = require("fs");
|
||||
const {getSettings} = require("./util-server");
|
||||
const {Notification} = require("./notification")
|
||||
const args = require('args-parser')(process.argv);
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
|
||||
const version = require('../package.json').version;
|
||||
const hostname = args.host || "0.0.0.0"
|
||||
const port = args.port || 3001
|
||||
console.log("Importing 3rd-party libraries")
|
||||
debug("Importing express");
|
||||
const express = require("express");
|
||||
debug("Importing socket.io");
|
||||
const { Server } = require("socket.io");
|
||||
debug("Importing redbean-node");
|
||||
const { R } = require("redbean-node");
|
||||
debug("Importing jsonwebtoken");
|
||||
const jwt = require("jsonwebtoken");
|
||||
debug("Importing http-graceful-shutdown");
|
||||
const gracefulShutdown = require("http-graceful-shutdown");
|
||||
debug("Importing prometheus-api-metrics");
|
||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||
|
||||
console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
console.info("Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
|
||||
// SSL
|
||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
|
||||
// Demo Mode?
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
if (demoMode) {
|
||||
console.log("==== Demo Mode ====");
|
||||
}
|
||||
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
Database.path = Database.dataDir + "kuma.db";
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
}
|
||||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
const app = express();
|
||||
|
||||
let server;
|
||||
|
||||
if (sslKey && sslCert) {
|
||||
console.log("Server Type: HTTPS");
|
||||
server = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
}, app);
|
||||
} else {
|
||||
console.log("Server Type: HTTP");
|
||||
server = http.createServer(app);
|
||||
}
|
||||
|
||||
const io = new Server(server);
|
||||
app.use(express.json())
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
* @type {number}
|
||||
*/
|
||||
let totalClient = 0;
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
let monitorList = {};
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
*/
|
||||
let needSetup = false;
|
||||
|
||||
/**
|
||||
* Cache Index HTML
|
||||
* @type {string}
|
||||
*/
|
||||
let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
|
||||
(async () => {
|
||||
await initDatabase();
|
||||
|
||||
app.use('/', express.static("dist"));
|
||||
console.log("Adding route")
|
||||
|
||||
app.get('*', function(request, response, next) {
|
||||
response.sendFile(process.cwd() + '/dist/index.html');
|
||||
// Normal Router here
|
||||
|
||||
// Robots.txt
|
||||
app.get("/robots.txt", async (_request, response) => {
|
||||
let txt = "User-agent: *\nDisallow:";
|
||||
if (! await setting("searchEngineIndex")) {
|
||||
txt += " /";
|
||||
}
|
||||
response.setHeader("Content-Type", "text/plain");
|
||||
response.send(txt);
|
||||
});
|
||||
|
||||
io.on('connection', async (socket) => {
|
||||
// Basic Auth Router here
|
||||
|
||||
// Prometheus API metrics /metrics
|
||||
// With Basic Auth using the first user's username/password
|
||||
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
||||
|
||||
app.use("/", express.static("dist"));
|
||||
|
||||
// Universal Route Handler, must be at the end
|
||||
app.get("*", async (_request, response) => {
|
||||
response.send(indexHTML);
|
||||
});
|
||||
|
||||
console.log("Adding socket handler")
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
})
|
||||
|
||||
console.log('a user connected');
|
||||
totalClient++;
|
||||
|
||||
if (needSetup) {
|
||||
@@ -48,12 +159,13 @@ let needSetup = false;
|
||||
socket.emit("setup")
|
||||
}
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('user disconnected');
|
||||
socket.on("disconnect", () => {
|
||||
totalClient--;
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Public API
|
||||
// ***************************
|
||||
|
||||
socket.on("loginByToken", async (token, callback) => {
|
||||
|
||||
@@ -63,11 +175,15 @@ let needSetup = false;
|
||||
console.log("Username from JWT: " + decoded.username)
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
decoded.username
|
||||
decoded.username,
|
||||
])
|
||||
|
||||
if (user) {
|
||||
await afterLogin(socket, user)
|
||||
debug("afterLogin")
|
||||
|
||||
afterLogin(socket, user)
|
||||
|
||||
debug("afterLogin ok")
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -75,13 +191,13 @@ let needSetup = false;
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "The user is inactive or deleted."
|
||||
msg: "The user is inactive or deleted.",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid token."
|
||||
msg: "Invalid token.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,32 +206,21 @@ let needSetup = false;
|
||||
socket.on("login", async (data, callback) => {
|
||||
console.log("Login")
|
||||
|
||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||
data.username
|
||||
])
|
||||
let user = await login(data.username, data.password)
|
||||
|
||||
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)
|
||||
if (user) {
|
||||
afterLogin(socket, user)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username
|
||||
}, jwtSecret)
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
})
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Incorrect username or password."
|
||||
msg: "Incorrect username or password.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,23 +251,22 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully."
|
||||
msg: "Added Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Auth Only API
|
||||
// ***************************
|
||||
|
||||
// Add a new monitor
|
||||
socket.on("add", async (monitor, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
@@ -171,6 +275,9 @@ let needSetup = false;
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
delete monitor.notificationIDList;
|
||||
|
||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
delete monitor.accepted_statuscodes;
|
||||
|
||||
bean.import(monitor)
|
||||
bean.user_id = socket.userID
|
||||
await R.store(bean)
|
||||
@@ -183,17 +290,18 @@ let needSetup = false;
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
monitorID: bean.id
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Edit a monitor
|
||||
socket.on("editMonitor", async (monitor, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
@@ -209,8 +317,15 @@ let needSetup = false;
|
||||
bean.url = monitor.url
|
||||
bean.interval = monitor.interval
|
||||
bean.hostname = monitor.hostname;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = monitor.port;
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
bean.maxredirects = monitor.maxredirects;
|
||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
|
||||
await R.store(bean)
|
||||
|
||||
@@ -225,14 +340,14 @@ let needSetup = false;
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved.",
|
||||
monitorID: bean.id
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -256,7 +371,7 @@ let needSetup = false;
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -270,13 +385,13 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Resumed Successfully."
|
||||
msg: "Resumed Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -289,14 +404,13 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Paused Successfully."
|
||||
msg: "Paused Successfully.",
|
||||
});
|
||||
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -314,12 +428,12 @@ let needSetup = false;
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully."
|
||||
msg: "Deleted Successfully.",
|
||||
});
|
||||
|
||||
await sendMonitorList(socket);
|
||||
@@ -327,7 +441,7 @@ let needSetup = false;
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -341,19 +455,16 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||
socket.userID
|
||||
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
|
||||
]);
|
||||
user.resetPassword(password.newPassword);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully."
|
||||
msg: "Password has been updated successfully.",
|
||||
})
|
||||
} else {
|
||||
throw new Error("Incorrect current password")
|
||||
@@ -362,25 +473,43 @@ let needSetup = false;
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getSettings", async (type, callback) => {
|
||||
socket.on("getSettings", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data: await getSettings(type),
|
||||
data: await getSettings("general"),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("setSettings", async (data, callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await setSettings("general", data)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -401,7 +530,7 @@ let needSetup = false;
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -421,7 +550,7 @@ let needSetup = false;
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -430,33 +559,71 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await Notification.send(notification, notification.name + " Testing")
|
||||
let msg = await Notification.send(notification, notification.name + " Testing")
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Sent Successfully"
|
||||
msg,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
callback(Notification.checkApprise());
|
||||
} catch (e) {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
|
||||
debug("added all socket handlers")
|
||||
|
||||
// ***************************
|
||||
// Better do anything after added all socket handlers here
|
||||
// ***************************
|
||||
|
||||
debug("check auto login")
|
||||
if (await setting("disableAuth")) {
|
||||
console.log("Disabled Auth: auto login to admin")
|
||||
afterLogin(socket, await R.findOne("user"))
|
||||
socket.emit("autoLogin")
|
||||
} else {
|
||||
debug("need auth")
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
console.log("Init the server")
|
||||
|
||||
server.once("error", async (err) => {
|
||||
console.error("Cannot listen: " + err.message);
|
||||
await Database.close();
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
|
||||
if (hostname) {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
} else {
|
||||
console.log(`Listening on ${port}`);
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
monitorID
|
||||
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
for (let notificationID in notificationIDList) {
|
||||
@@ -487,9 +654,11 @@ async function sendMonitorList(socket) {
|
||||
}
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
let list = await R.find("notification", " user_id = ? ", [
|
||||
socket.userID
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
@@ -497,6 +666,9 @@ async function sendNotificationList(socket) {
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
|
||||
timeLogger.print("Send Notification List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -505,21 +677,28 @@ async function afterLogin(socket, user) {
|
||||
socket.join(user.id)
|
||||
|
||||
let monitorList = await sendMonitorList(socket)
|
||||
sendNotificationList(socket)
|
||||
|
||||
await sleep(500);
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
await sendImportantHeartbeatList(socket, monitorID);
|
||||
await Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
|
||||
await sendNotificationList(socket)
|
||||
for (let monitorID in monitorList) {
|
||||
await sendImportantHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let monitorList = await R.find("monitor", " user_id = ? ", [
|
||||
userID
|
||||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||
userID,
|
||||
])
|
||||
|
||||
for (let monitor of monitorList) {
|
||||
@@ -536,36 +715,31 @@ function checkLogin(socket) {
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
const path = './data/kuma.db';
|
||||
|
||||
if (! fs.existsSync(path)) {
|
||||
console.log("Copy Database")
|
||||
fs.copyFileSync("./db/kuma.db", path);
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database")
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connect to Database")
|
||||
console.log("Connecting to Database")
|
||||
await Database.connect();
|
||||
console.log("Connected")
|
||||
|
||||
R.setup('sqlite', {
|
||||
filename: path
|
||||
});
|
||||
R.freeze(true)
|
||||
await R.autoloadModels("./server/model");
|
||||
// Patch the database
|
||||
await Database.patch()
|
||||
|
||||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
||||
"jwtSecret"
|
||||
"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("JWT secret is not found, generate one.");
|
||||
jwtSecretBean = await initJWTSecret();
|
||||
console.log("Stored JWT secret into database");
|
||||
} else {
|
||||
console.log("Load JWT secret from database.")
|
||||
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;
|
||||
@@ -581,11 +755,11 @@ async function startMonitor(userID, monitorID) {
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID
|
||||
userID,
|
||||
]);
|
||||
|
||||
let monitor = await R.findOne("monitor", " id = ? ", [
|
||||
monitorID
|
||||
monitorID,
|
||||
])
|
||||
|
||||
if (monitor.id in monitorList) {
|
||||
@@ -607,7 +781,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
userID
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (monitorID in monitorList) {
|
||||
@@ -622,41 +796,86 @@ async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ")
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io)
|
||||
monitorList[monitor.id] = monitor;
|
||||
}
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io);
|
||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||
await sleep(getRandomInt(300, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 100
|
||||
`, [
|
||||
monitorID
|
||||
monitorID,
|
||||
])
|
||||
|
||||
let result = [];
|
||||
|
||||
for (let bean of list) {
|
||||
result.unshift(bean.toJSON())
|
||||
result.unshift(bean.toJSON())
|
||||
}
|
||||
|
||||
socket.emit("heartbeatList", monitorID, result)
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`)
|
||||
}
|
||||
|
||||
async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
AND important = 1
|
||||
ORDER BY time DESC
|
||||
LIMIT 500
|
||||
`, [
|
||||
monitorID
|
||||
monitorID,
|
||||
])
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||
|
||||
socket.emit("importantHeartbeatList", monitorID, list)
|
||||
}
|
||||
|
||||
async function shutdownFunction(signal) {
|
||||
console.log("Shutdown requested");
|
||||
console.log("Called signal: " + signal);
|
||||
|
||||
console.log("Stopping all monitors")
|
||||
for (let id in monitorList) {
|
||||
let monitor = monitorList[id]
|
||||
monitor.stop()
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful shutdown successfully!");
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// Catch unexpected errors here
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
});
|
||||
|
@@ -1,6 +1,29 @@
|
||||
const tcpp = require('tcp-ping');
|
||||
const tcpp = require("tcp-ping");
|
||||
const Ping = require("./ping-lite");
|
||||
const {R} = require("redbean-node");
|
||||
const { R } = require("redbean-node");
|
||||
const { debug } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.initJWTSecret = async () => {
|
||||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
||||
"jwtSecret",
|
||||
]);
|
||||
|
||||
if (! jwtSecretBean) {
|
||||
jwtSecretBean = R.dispense("setting");
|
||||
jwtSecretBean.key = "jwtSecret";
|
||||
}
|
||||
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||
await R.store(jwtSecretBean);
|
||||
return jwtSecretBean;
|
||||
}
|
||||
|
||||
exports.tcping = function (hostname, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -8,7 +31,7 @@ exports.tcping = function (hostname, port) {
|
||||
address: hostname,
|
||||
port: port,
|
||||
attempts: 1,
|
||||
}, function(err, data) {
|
||||
}, function (err, data) {
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
@@ -23,15 +46,30 @@ exports.tcping = function (hostname, port) {
|
||||
});
|
||||
}
|
||||
|
||||
exports.ping = function (hostname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname);
|
||||
exports.ping = async (hostname) => {
|
||||
try {
|
||||
return await exports.pingAsync(hostname);
|
||||
} catch (e) {
|
||||
// If the host cannot be resolved, try again with ipv6
|
||||
if (e.message.includes("service not known")) {
|
||||
return await exports.pingAsync(hostname, true);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ping.send(function(err, ms) {
|
||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname, {
|
||||
ipv6
|
||||
});
|
||||
|
||||
ping.send(function (err, ms, stdout) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
reject(err);
|
||||
} else if (ms === null) {
|
||||
reject(new Error("timeout"))
|
||||
reject(new Error(stdout))
|
||||
} else {
|
||||
resolve(Math.round(ms))
|
||||
}
|
||||
@@ -39,22 +77,197 @@ exports.ping = function (hostname) {
|
||||
});
|
||||
}
|
||||
|
||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([resolver_server]);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rrtype == "PTR") {
|
||||
resolver.reverse(hostname, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolver.resolve(hostname, rrtype, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.setting = async function (key) {
|
||||
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
key
|
||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
key,
|
||||
]);
|
||||
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
debug(`Get Setting: ${key}: ${v}`)
|
||||
return v;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
])
|
||||
if (!bean) {
|
||||
bean = R.dispense("setting")
|
||||
bean.key = key;
|
||||
}
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean)
|
||||
}
|
||||
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
|
||||
type
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
type,
|
||||
])
|
||||
|
||||
let result = {};
|
||||
|
||||
for (let row of list) {
|
||||
result[row.key] = row.value;
|
||||
try {
|
||||
result[row.key] = JSON.parse(row.value);
|
||||
} catch (e) {
|
||||
result[row.key] = row.value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.setSettings = async function (type, data) {
|
||||
let keyList = Object.keys(data);
|
||||
|
||||
let promiseList = [];
|
||||
|
||||
for (let key of keyList) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key
|
||||
]);
|
||||
|
||||
if (bean == null) {
|
||||
bean = R.dispense("setting");
|
||||
bean.type = type;
|
||||
bean.key = key;
|
||||
}
|
||||
|
||||
if (bean.type === type) {
|
||||
bean.value = JSON.stringify(data[key]);
|
||||
promiseList.push(R.store(bean))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
// return an object containing the certificate information
|
||||
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
return -daysRemaining;
|
||||
}
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const {
|
||||
valid_from,
|
||||
valid_to,
|
||||
subjectaltname,
|
||||
issuer,
|
||||
fingerprint,
|
||||
} = res.request.res.socket.getPeerCertificate(false);
|
||||
|
||||
if (!valid_from || !valid_to || !subjectaltname) {
|
||||
throw {
|
||||
message: "No TLS certificate in response",
|
||||
};
|
||||
}
|
||||
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
const validTo = new Date(valid_to);
|
||||
|
||||
const validFor = subjectaltname
|
||||
.replace(/DNS:|IP Address:/g, "")
|
||||
.split(", ");
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
||||
|
||||
return {
|
||||
valid,
|
||||
validFor,
|
||||
validTo,
|
||||
daysRemaining,
|
||||
issuer,
|
||||
fingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the provided status code is within the accepted ranges
|
||||
// Param: status - the status code to check
|
||||
// Param: accepted_codes - an array of accepted status codes
|
||||
// Return: true if the status code is within the accepted ranges, false otherwise
|
||||
// Will throw an error if the provided status code is not a valid range string or code string
|
||||
|
||||
exports.checkStatusCode = function (status, accepted_codes) {
|
||||
if (accepted_codes == null || accepted_codes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const code_range of accepted_codes) {
|
||||
const code_range_split = code_range.split("-").map(string => parseInt(string));
|
||||
if (code_range_split.length === 1) {
|
||||
if (status === code_range_split[0]) {
|
||||
return true;
|
||||
}
|
||||
} else if (code_range_split.length === 2) {
|
||||
if (status >= code_range_split[0] && status <= code_range_split[1]) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid status code range");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
exports.getTotalClientInRoom = (io, roomName) => {
|
||||
|
||||
const sockets = io.sockets;
|
||||
|
||||
if (! sockets) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const adapter = sockets.adapter;
|
||||
|
||||
if (! adapter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const room = adapter.rooms.get(roomName);
|
||||
|
||||
if (room) {
|
||||
return room.size;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Common functions - can be used in frontend or backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -3,11 +3,5 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
|
@@ -2,12 +2,48 @@
|
||||
@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;
|
||||
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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.dark & {
|
||||
box-shadow: 0 15px 70px rgb(0 0 0);
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.VuePagination__count {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
overflow: hidden;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||
//overflow: hidden; // Forget why add this, but multiple select hide by this
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -29,10 +65,226 @@
|
||||
background-color: $highlight;
|
||||
border-color: $highlight;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(3px);
|
||||
.btn-warning {
|
||||
color: white;
|
||||
|
||||
&:hover, &:active, &:focus, &.active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
color: white;
|
||||
|
||||
&:hover, &:active, &:focus, &.active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
.shadow-box {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
margin-top: 0 !important;
|
||||
padding: 4px 10px !important;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
td:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td:nth-child(-n+3) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid $dark-font-color;
|
||||
display: block;
|
||||
padding: 4px;
|
||||
|
||||
.badge {
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Theme override here
|
||||
.dark {
|
||||
background-color: #090c10;
|
||||
color: $dark-font-color;
|
||||
|
||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||
background: $dark-border-color;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
background-color: #131a21;
|
||||
}
|
||||
|
||||
a,
|
||||
.table,
|
||||
.nav-link {
|
||||
color: $dark-font-color;
|
||||
|
||||
&.btn-info {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-control:focus,
|
||||
.form-select,
|
||||
.form-select:focus {
|
||||
color: $dark-font-color;
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
--bs-table-accent-bg: #070a10;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
color: $dark-font-color2;
|
||||
|
||||
&:hover, &:active, &:focus, &.active {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
box-shadow: none;
|
||||
filter: invert(1);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-color: $dark-bg;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-color: $dark-bg;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
.page-item.disabled .page-link {
|
||||
background-color: $dark-bg;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
background-color: $dark-bg;
|
||||
border-color: $dark-border-color;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
// Multiselect
|
||||
.multiselect__tags {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__input, .multiselect__single {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
tbody {
|
||||
.shadow-box {
|
||||
background-color: $dark-bg2;
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Transitions
|
||||
*/
|
||||
|
||||
// page-change
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
@@ -1,7 +1,18 @@
|
||||
$primary: #5CDD8B;
|
||||
$danger: #DC3545;
|
||||
$primary: #5cdd8b;
|
||||
$danger: #dc3545;
|
||||
$warning: #f8a306;
|
||||
$link-color: #111;
|
||||
$border-radius: 50rem;
|
||||
|
||||
$highlight: #7ce8a4;
|
||||
$highlight-white: #e7faec;
|
||||
|
||||
$dark-font-color: #b1b8c0;
|
||||
$dark-font-color2: #020b05;
|
||||
$dark-bg: #0d1117;
|
||||
$dark-bg2: #070a10;
|
||||
$dark-border-color: #1d2634;
|
||||
|
||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
|
||||
|
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="modal fade" tabindex="-1" ref="modal">
|
||||
<div ref="modal" class="modal fade" tabindex="-1">
|
||||
<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>
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Confirm") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></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>
|
||||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||
{{ yesText }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ noText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,17 +25,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from 'bootstrap'
|
||||
import { Modal } from "bootstrap"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
btnStyle: {
|
||||
type: String,
|
||||
default: "btn-primary"
|
||||
}
|
||||
default: "btn-primary",
|
||||
},
|
||||
yesText: {
|
||||
type: String,
|
||||
default: "Yes", // TODO: No idea what to translate this
|
||||
},
|
||||
noText: {
|
||||
type: String,
|
||||
default: "No",
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
modal: null
|
||||
modal: null,
|
||||
}),
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
@@ -39,12 +53,8 @@ export default {
|
||||
this.modal.show()
|
||||
},
|
||||
yes() {
|
||||
this.$emit('yes');
|
||||
}
|
||||
}
|
||||
this.$emit("yes");
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -3,26 +3,22 @@
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
|
||||
import {sleep} from "../../server/util";
|
||||
import { sleep } from "../util.ts"
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
value: [String, Number],
|
||||
time: {
|
||||
Number,
|
||||
type: Number,
|
||||
default: 0.3,
|
||||
},
|
||||
unit: {
|
||||
String,
|
||||
type: String,
|
||||
default: "ms",
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.output = this.value;
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -32,14 +28,10 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
isNum() {
|
||||
return typeof this.value === 'number'
|
||||
}
|
||||
return typeof this.value === "number"
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -61,9 +53,11 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.output = this.value;
|
||||
},
|
||||
|
||||
methods: {},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -4,9 +4,9 @@
|
||||
|
||||
<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
|
||||
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)
|
||||
@@ -14,17 +14,20 @@ dayjs.extend(relativeTime)
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
dateOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayText() {
|
||||
let format = "YYYY-MM-DD HH:mm:ss";
|
||||
return dayjs.utc(this.value).tz(this.$root.timezone).format(format)
|
||||
if (this.dateOnly) {
|
||||
return this.$root.date(this.value);
|
||||
} else {
|
||||
return this.$root.datetime(this.value);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -1,28 +1,30 @@
|
||||
<template>
|
||||
<div class="wrap" :style="wrapStyle" ref="wrap">
|
||||
<div ref="wrap" class="wrap" :style="wrapStyle">
|
||||
<div class="hp-bar-big" :style="barStyle">
|
||||
<div
|
||||
class="beat"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
|
||||
:style="beatStyle"
|
||||
v-for="(beat, index) in shortBeatList"
|
||||
:key="index"
|
||||
:title="beat.msg">
|
||||
</div>
|
||||
class="beat"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||
:style="beatStyle"
|
||||
:title="getBeatTitle(beat)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: "big"
|
||||
default: "big",
|
||||
},
|
||||
monitorId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
monitorId: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -34,9 +36,92 @@ export default {
|
||||
maxBeat: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
beatList() {
|
||||
return this.$root.heartbeatList[this.monitorId]
|
||||
},
|
||||
|
||||
shortBeatList() {
|
||||
if (! this.beatList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let placeholders = [];
|
||||
|
||||
let start = this.beatList.length - this.maxBeat;
|
||||
|
||||
if (this.move) {
|
||||
start = start - 1;
|
||||
}
|
||||
|
||||
if (start < 0) {
|
||||
// Add empty placeholder
|
||||
for (let i = start; i < 0; i++) {
|
||||
placeholders.push(0)
|
||||
}
|
||||
start = 0;
|
||||
}
|
||||
|
||||
return placeholders.concat(this.beatList.slice(start))
|
||||
},
|
||||
|
||||
wrapStyle() {
|
||||
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
|
||||
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
|
||||
|
||||
return {
|
||||
padding: `${topBottom}px ${leftRight}px`,
|
||||
width: "100%",
|
||||
}
|
||||
},
|
||||
|
||||
barStyle() {
|
||||
if (this.move && this.shortBeatList.length > this.maxBeat) {
|
||||
let width = -(this.beatWidth + this.beatMargin * 2);
|
||||
|
||||
return {
|
||||
transition: "all ease-in-out 0.25s",
|
||||
transform: `translateX(${width}px)`,
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
transform: "translateX(0)",
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
beatStyle() {
|
||||
return {
|
||||
width: this.beatWidth + "px",
|
||||
height: this.beatHeight + "px",
|
||||
margin: this.beatMargin + "px",
|
||||
"--hover-scale": this.hoverScale,
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
beatList: {
|
||||
handler(val, oldVal) {
|
||||
this.move = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.move = false;
|
||||
}, 300)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
beforeMount() {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.size === "small") {
|
||||
this.beatWidth = 5.6;
|
||||
@@ -52,98 +137,16 @@ export default {
|
||||
if (this.$refs.wrap) {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
||||
}
|
||||
},
|
||||
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
||||
}
|
||||
},
|
||||
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">
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.wrap {
|
||||
@@ -166,6 +169,10 @@ export default {
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: $warning;
|
||||
}
|
||||
|
||||
&:not(.empty):hover {
|
||||
transition: all ease-in-out 0.15s;
|
||||
opacity: 0.8;
|
||||
@@ -174,4 +181,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.hp-bar-big .beat.empty {
|
||||
background-color: #848484;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -2,31 +2,32 @@
|
||||
<div class="form-container">
|
||||
<div class="form">
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
<h1 class="h3 mb-3 fw-normal"></h1>
|
||||
<h1 class="h3 mb-3 fw-normal" />
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username">
|
||||
<label for="floatingInput">Username</label>
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||
<label for="floatingInput">{{ $t("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>
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 mt-3" >
|
||||
<label>
|
||||
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||
<div class="form-check">
|
||||
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||
|
||||
<label class="form-check-label" for="remember">
|
||||
Remember me
|
||||
{{ $t("Remember me") }}
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button>
|
||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
|
||||
{{ $t("Login") }}
|
||||
</button>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok">
|
||||
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
|
||||
{{ res.msg }}
|
||||
</div>
|
||||
</form>
|
||||
@@ -52,8 +53,8 @@ export default {
|
||||
this.processing = false;
|
||||
this.res = res;
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
143
src/components/MonitorList.vue
Normal file
143
src/components/MonitorList.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
},
|
||||
props: {
|
||||
scrollbar: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
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;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
&.scrollbar {
|
||||
min-height: calc(100vh - 240px);
|
||||
max-height: calc(100vh - 30px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 13px 15px 10px 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monitorItem {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -1,80 +1,96 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
|
||||
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Setup Notification") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">{{ $t("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="rocket.chat">Rocket.chat</option>
|
||||
<option value="pushover">Pushover</option>
|
||||
<option value="pushy">Pushy</option>
|
||||
<option value="octopush">Octopush</option>
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
<option value="line">Line Messenger</option>
|
||||
<option value="mattermost">Mattermost</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type === 'telegram'">
|
||||
<div class="mb-3">
|
||||
<label for="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>
|
||||
</select>
|
||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
|
||||
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
|
||||
Auto Get
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<template v-if="notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template 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">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
|
||||
<input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||
<select 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 id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
|
||||
<option value="json">
|
||||
application/json
|
||||
</option>
|
||||
<option value="form-data">
|
||||
multipart/form-data
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
@@ -87,70 +103,76 @@
|
||||
<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">
|
||||
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" id="port" v-model="notification.smtpPort" required min="0" max="65535" step="1">
|
||||
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
||||
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="secure">
|
||||
Secure
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Generally, true for 465, false for other ports.</div>
|
||||
<div 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">
|
||||
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false">
|
||||
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<input type="email" class="form-control" id="from-email" required v-model="notification.smtpFrom" autocomplete="false">
|
||||
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input type="email" class="form-control" id="to-email" required v-model="notification.smtpTo" autocomplete="false">
|
||||
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||
<input 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>
|
||||
<input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</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">
|
||||
|
||||
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">Number</label>
|
||||
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber">
|
||||
|
||||
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients">
|
||||
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
You need to have a signal client with REST API.
|
||||
@@ -171,47 +193,317 @@
|
||||
</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 class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-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 class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">Priority</label>
|
||||
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
|
||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'rocket.chat'">
|
||||
<div class="mb-3">
|
||||
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||
<label for="rocket-username" class="form-label">Username</label>
|
||||
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
|
||||
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
|
||||
<label for="rocket-channel" class="form-label">Channel Name</label>
|
||||
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
|
||||
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Rocket.chat 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 === 'mattermost'">
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">Username</label>
|
||||
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">Channel Name</label>
|
||||
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. 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;">
|
||||
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
|
||||
</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> Note: emoji takes preference over Icon URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushy'">
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'octopush'">
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<input id="octopush-key" v-model="notification.octopushAPIKey" type="text" class="form-control" required>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-type-sms" class="form-label">SMS Type</label>
|
||||
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
|
||||
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
|
||||
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
|
||||
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
|
||||
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">Priority</label>
|
||||
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you want to send notifications to different devices, fill out Device field.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'apprise'">
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||
<p>
|
||||
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Status:
|
||||
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushbullet'">
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">Access Token</label>
|
||||
<input id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'line'">
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||
<input id="line-channel-access-token" v-model="notification.lineChannelAccessToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Basic Settings</b>
|
||||
</div>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Messaging API</b>
|
||||
</div>
|
||||
<div class="form-text" style="margin-top: 8px;">
|
||||
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||
</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>
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||
{{ $t("Test") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
{{ $t("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>
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteNotification">
|
||||
{{ $t("deleteNotificationMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from 'bootstrap'
|
||||
import { ucfirst } from "../../server/util";
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import axios from "axios";
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useToast } from "vue-toastification"
|
||||
import Confirm from "./Confirm.vue";
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {Confirm},
|
||||
props: {
|
||||
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
@@ -220,18 +512,43 @@ export default {
|
||||
notification: {
|
||||
name: "",
|
||||
type: null,
|
||||
gotifyPriority: 8,
|
||||
},
|
||||
appriseInstalled: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
telegramGetUpdatesURL() {
|
||||
let token = "<YOUR BOT TOKEN HERE>"
|
||||
|
||||
if (this.notification.telegramBotToken) {
|
||||
token = this.notification.telegramBotToken;
|
||||
}
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"notification.type"(to, from) {
|
||||
let oldName;
|
||||
|
||||
if (from) {
|
||||
oldName = `My ${ucfirst(from)} Alert (1)`;
|
||||
} else {
|
||||
oldName = "";
|
||||
}
|
||||
|
||||
if (! this.notification.name || this.notification.name === oldName) {
|
||||
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
|
||||
// TODO: for edit
|
||||
this.$root.getSocket().emit("getSettings", "notification", (data) => {
|
||||
// this.notification = data
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -259,6 +576,7 @@ export default {
|
||||
|
||||
// Default set to Telegram
|
||||
this.notification.type = "telegram"
|
||||
this.notification.gotifyPriority = 8
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
@@ -322,35 +640,15 @@ export default {
|
||||
},
|
||||
|
||||
},
|
||||
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 lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
176
src/components/PingChart.vue
Normal file
176
src/components/PingChart.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import "chartjs-adapter-dayjs";
|
||||
import { LineChart } from "vue-chart-3";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||
|
||||
export default {
|
||||
components: { LineChart },
|
||||
props: {
|
||||
monitorId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Configurable filtering on top of the returned data
|
||||
chartPeriodHrs: 6,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
chartOptions() {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
onResize: (chart) => {
|
||||
chart.canvas.parentNode.style.position = "relative";
|
||||
if (screen.width < 576) {
|
||||
chart.canvas.parentNode.style.height = "275px";
|
||||
} else if (screen.width < 768) {
|
||||
chart.canvas.parentNode.style.height = "320px";
|
||||
} else if (screen.width < 992) {
|
||||
chart.canvas.parentNode.style.height = "300px";
|
||||
} else {
|
||||
chart.canvas.parentNode.style.height = "250px";
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 30,
|
||||
top: 30,
|
||||
bottom: 10,
|
||||
},
|
||||
},
|
||||
|
||||
elements: {
|
||||
point: {
|
||||
// Hide points on chart unless mouse-over
|
||||
radius: 0,
|
||||
hitRadius: 100,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
minUnit: "minute",
|
||||
round: "second",
|
||||
tooltipFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
displayFormats: {
|
||||
minute: "HH:mm",
|
||||
hour: "MM-DD HH:mm",
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 30,
|
||||
},
|
||||
grid: {
|
||||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
|
||||
offset: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: this.$t("respTime"),
|
||||
},
|
||||
offset: false,
|
||||
grid: {
|
||||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
display: false,
|
||||
position: "right",
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
min: 0,
|
||||
max: 1,
|
||||
offset: false,
|
||||
},
|
||||
},
|
||||
bounds: "ticks",
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: "nearest",
|
||||
intersect: false,
|
||||
padding: 10,
|
||||
backgroundColor: this.$root.theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)",
|
||||
bodyColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
|
||||
titleColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
|
||||
filter: function (tooltipItem) {
|
||||
return tooltipItem.datasetIndex === 0; // Hide tooltip on Bar Chart
|
||||
},
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
|
||||
},
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
chartData() {
|
||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
||||
if (this.monitorId in this.$root.heartbeatList) {
|
||||
this.$root.heartbeatList[this.monitorId]
|
||||
.filter(
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
|
||||
.map((beat) => {
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: beat.status === 0 ? 1 : 0,
|
||||
})
|
||||
});
|
||||
}
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
// Line Chart
|
||||
data: pingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#5CDD8B",
|
||||
backgroundColor: "#5CDD8B38",
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
// Bar Chart
|
||||
type: "bar",
|
||||
data: downData,
|
||||
borderColor: "#00000000",
|
||||
backgroundColor: "#DC354568",
|
||||
yAxisID: "y1",
|
||||
barThickness: "flex",
|
||||
barPercentage: 1,
|
||||
categoryPercentage: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -5,35 +5,47 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
status: Number
|
||||
status: Number,
|
||||
},
|
||||
|
||||
computed: {
|
||||
color() {
|
||||
if (this.status === 0) {
|
||||
return "danger"
|
||||
} else if (this.status === 1) {
|
||||
return "primary"
|
||||
} else {
|
||||
return "secondary"
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "primary"
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
},
|
||||
|
||||
text() {
|
||||
if (this.status === 0) {
|
||||
return "Down"
|
||||
} else if (this.status === 1) {
|
||||
return "Up"
|
||||
} else {
|
||||
return "Unknown"
|
||||
return this.$t("Down");
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return this.$t("Up");
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return this.$t("Pending");
|
||||
}
|
||||
|
||||
return this.$t("Unknown");
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
width: 45px;
|
||||
width: 64px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,10 +5,10 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
monitor : Object,
|
||||
monitor: Object,
|
||||
type: String,
|
||||
pill: {
|
||||
Boolean,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
@@ -20,42 +20,50 @@ export default {
|
||||
|
||||
if (this.$root.uptimeList[key] !== undefined) {
|
||||
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
||||
} else {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
return this.$t("notAvailableShort")
|
||||
},
|
||||
|
||||
color() {
|
||||
if (this.lastHeartBeat.status === 0) {
|
||||
return "danger"
|
||||
} else if (this.lastHeartBeat.status === 1) {
|
||||
return "primary"
|
||||
} else {
|
||||
return "secondary"
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 1) {
|
||||
return "primary"
|
||||
}
|
||||
|
||||
if (this.lastHeartBeat.status === 2) {
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
},
|
||||
|
||||
lastHeartBeat() {
|
||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
||||
} else {
|
||||
return { status: -1 }
|
||||
}
|
||||
|
||||
return {
|
||||
status: -1,
|
||||
}
|
||||
},
|
||||
|
||||
className() {
|
||||
if (this.pill) {
|
||||
return `badge rounded-pill bg-${this.color}`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
min-width: 62px;
|
||||
}
|
||||
</style>
|
||||
|
10
src/icon.js
Normal file
10
src/icon.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp } from "@fortawesome/free-solid-svg-icons"
|
||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||
|
||||
// Add Free Font Awesome Icons here
|
||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp);
|
||||
|
||||
export { FontAwesomeIcon }
|
18
src/languages/README.md
Normal file
18
src/languages/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# How to translate
|
||||
|
||||
1. Fork this repo.
|
||||
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
|
||||
3. `npm run update-language-files --base-lang=de-DE`
|
||||
6. Your language file should be filled in. You can translate now.
|
||||
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||
8. Import your language file in `src/main.js` and add it to `languageList` constant.
|
||||
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||
|
||||
|
||||
|
||||
One of good examples:
|
||||
https://github.com/louislam/uptime-kuma/pull/316/files
|
||||
|
||||
|
||||
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
||||
|
112
src/languages/da-DK.js
Normal file
112
src/languages/da-DK.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Danish",
|
||||
Settings: "Indstillinger",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Opdatering tilgængelig",
|
||||
Language: "Sprog",
|
||||
Appearance: "Udseende",
|
||||
Theme: "Tema",
|
||||
General: "Generelt",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Tjek efter opdateringer på Github",
|
||||
List: "Liste",
|
||||
Add: "Tilføj",
|
||||
"Add New Monitor": "Tilføj ny Overvåger",
|
||||
"Quick Stats": "Oversigt",
|
||||
Up: "Aktiv",
|
||||
Down: "Inaktiv",
|
||||
Pending: "Afventer",
|
||||
Unknown: "Ukendt",
|
||||
Pause: "Pause",
|
||||
pauseDashboardHome: "Pauset",
|
||||
Name: "Navn",
|
||||
Status: "Status",
|
||||
DateTime: "Dato / Tid",
|
||||
Message: "Beskeder",
|
||||
"No important events": "Inden vigtige begivenheder",
|
||||
Resume: "Fortsæt",
|
||||
Edit: "Rediger",
|
||||
Delete: "Slet",
|
||||
Current: "Aktuelt",
|
||||
Uptime: "Oppetid",
|
||||
"Cert Exp.": "Certifikatets udløb",
|
||||
days: "Dage",
|
||||
day: "Dag",
|
||||
"-day": "-Dage",
|
||||
hour: "Timer",
|
||||
"-hour": "-Timer",
|
||||
checkEverySecond: "Tjek hvert {0} sekund",
|
||||
"Avg.": "Gennemsnit",
|
||||
Response: " Respons",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Overvåger Type",
|
||||
Keyword: "Nøgleord",
|
||||
"Friendly Name": "Visningsnavn",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Taktinterval",
|
||||
Retries: "Gentagelser",
|
||||
retriesDescription: "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.",
|
||||
Advanced: "Avanceret",
|
||||
ignoreTLSError: "Ignorere TLS/SSL web fejl",
|
||||
"Upside Down Mode": "Omvendt tilstand",
|
||||
upsideDownModeDescription: "Håndter tilstanden omvendt. Hvis tjenesten er tilgængelig, vises den som inaktiv.",
|
||||
"Max. Redirects": "Maks. Omdirigeringer",
|
||||
maxRedirectDescription: "Maksimalt antal omdirigeringer, der skal følges. Indstil til 0 for at deaktivere omdirigeringer.",
|
||||
"Accepted Status Codes": "Tilladte HTTP-Statuskoder",
|
||||
acceptedStatusCodesDescription: "Vælg de statuskoder, der stadig skal vurderes som vellykkede.",
|
||||
Save: "Gem",
|
||||
Notifications: "Underretninger",
|
||||
"Not available, please setup.": "Ikke tilgængelige, opsæt venligst.",
|
||||
"Setup Notification": "Opsæt underretninger",
|
||||
Light: "Lys",
|
||||
Dark: "Mørk",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Tema - Tidslinje",
|
||||
Normal: "Normal",
|
||||
Bottom: "Bunden",
|
||||
None: "Ingen",
|
||||
Timezone: "Tidszone",
|
||||
"Search Engine Visibility": "Søgemaskine synlighed",
|
||||
"Allow indexing": "Tillad indeksering",
|
||||
"Discourage search engines from indexing site": "Frabed søgemaskiner at indeksere webstedet",
|
||||
"Change Password": "Ændre adgangskode",
|
||||
"Current Password": "Nuværende adgangskode",
|
||||
"New Password": "Ny adgangskode",
|
||||
"Repeat New Password": "Gentag den nye adgangskode",
|
||||
passwordNotMatchMsg: "Adgangskoderne er ikke ens.",
|
||||
"Update Password": "Opdater adgangskode",
|
||||
"Disable Auth": "Deaktiver autentificering",
|
||||
"Enable Auth": "Aktiver autentificering",
|
||||
Logout: "Log ud",
|
||||
notificationDescription: "Tildel underretninger til Overvåger(e), så denne funktion træder i kraft.",
|
||||
Leave: "Verlassen",
|
||||
"I understand, please disable": "Jeg er indforstået, deaktiver venligst",
|
||||
Confirm: "Bekræft",
|
||||
Yes: "Ja",
|
||||
No: "Nej",
|
||||
Username: "Brugernavn",
|
||||
Password: "Adgangskode",
|
||||
"Remember me": "Husk mig",
|
||||
Login: "Log ind",
|
||||
"No Monitors, please": "Ingen Overvågere",
|
||||
"add one": "tilføj en",
|
||||
"Notification Type": "Underretningstype",
|
||||
Email: "E-Mail",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Certifikatoplysninger",
|
||||
keywordDescription: "Søg efter et søgeord i almindelig HTML- eller JSON -output. Bemærk, at der skelnes mellem store og små bogstaver.",
|
||||
deleteMonitorMsg: "Er du sikker på, at du vil slette overvågeren?",
|
||||
deleteNotificationMsg: "Er du sikker på, at du vil slette denne underretning for alle overvågere? ",
|
||||
resoverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.",
|
||||
"Resolver Server": "Navne-server",
|
||||
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
|
||||
"Last Result": "Seneste resultat",
|
||||
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
|
||||
"Create your admin account": "Opret din administratorkonto",
|
||||
"Repeat Password": "Gentag adgangskoden",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/de-DE.js
Normal file
112
src/languages/de-DE.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "German",
|
||||
Settings: "Einstellungen",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Update Verfügbar",
|
||||
Language: "Sprache",
|
||||
Appearance: "Erscheinung",
|
||||
Theme: "Thema",
|
||||
General: "Allgemein",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Überprüfen von Updates auf Github",
|
||||
List: "Liste",
|
||||
Add: "Hinzufügen",
|
||||
"Add New Monitor": "Neuer Monitor",
|
||||
"Quick Stats": "Übersicht",
|
||||
Up: "Aktiv",
|
||||
Down: "Inaktiv",
|
||||
Pending: "Ausstehend",
|
||||
Unknown: "Unbekannt",
|
||||
Pause: "Pausieren",
|
||||
pauseDashboardHome: "Pausiert",
|
||||
Name: "Name",
|
||||
Status: "Status",
|
||||
DateTime: "Datum / Uhrzeit",
|
||||
Message: "Nachricht",
|
||||
"No important events": "Keine wichtigen Ereignisse",
|
||||
Resume: "Fortsetzen",
|
||||
Edit: "Bearbeiten",
|
||||
Delete: "Löschen",
|
||||
Current: "Aktuell",
|
||||
Uptime: "Verfügbarkeit",
|
||||
"Cert Exp.": "Zertifikatsablauf",
|
||||
days: "Tage",
|
||||
day: "Tag",
|
||||
"-day": "-Tage",
|
||||
hour: "Stunde",
|
||||
"-hour": "-Stunden",
|
||||
checkEverySecond: "Überprüfe alle {0} Sekunden",
|
||||
"Avg.": "Durchschn. ",
|
||||
Response: " Antwortzeit",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Monitor Typ",
|
||||
Keyword: "Schlüsselwort",
|
||||
"Friendly Name": "Anzeigename",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Taktintervall",
|
||||
Retries: "Wiederholungen",
|
||||
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
||||
Advanced: "Erweitert",
|
||||
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten",
|
||||
"Upside Down Mode": "Umgedrehter Modus",
|
||||
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.",
|
||||
"Max. Redirects": "Max. Weiterleitungen",
|
||||
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.",
|
||||
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
|
||||
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
|
||||
Save: "Speichern",
|
||||
Notifications: "Benachrichtigungen",
|
||||
"Not available, please setup.": "Keine verfügbar, bitte einrichten.",
|
||||
"Setup Notification": "Benachrichtigung einrichten",
|
||||
Light: "Hell",
|
||||
Dark: "Dunkel",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Thema - Taktleiste",
|
||||
Normal: "Normal",
|
||||
Bottom: "Unten",
|
||||
None: "Keine",
|
||||
Timezone: "Zeitzone",
|
||||
"Search Engine Visibility": "Suchmaschinensichtbarkeit",
|
||||
"Allow indexing": "Indizierung zulassen",
|
||||
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
|
||||
"Change Password": "Passwort ändern",
|
||||
"Current Password": "Dezeitiges Passwort",
|
||||
"New Password": "Neues Passwort",
|
||||
"Repeat New Password": "Wiederhole neues Passwort",
|
||||
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
|
||||
"Update Password": "Ändere Passwort",
|
||||
"Disable Auth": "Authentifizierung deaktivieren",
|
||||
"Enable Auth": "Authentifizierung aktivieren",
|
||||
Logout: "Ausloggen",
|
||||
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
|
||||
Leave: "Verlassen",
|
||||
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
|
||||
Confirm: "Bestätige",
|
||||
Yes: "Ja",
|
||||
No: "Nein",
|
||||
Username: "Benutzername",
|
||||
Password: "Passwort",
|
||||
"Remember me": "Passwort merken",
|
||||
Login: "Einloggen",
|
||||
"No Monitors, please": "Keine Monitore, bitte",
|
||||
"add one": "hinzufügen",
|
||||
"Notification Type": "Benachrichtigungs Dienst",
|
||||
Email: "E-Mail",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Zertifikatsinfo",
|
||||
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.",
|
||||
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?",
|
||||
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
|
||||
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
|
||||
"Resolver Server": "Auflösungsserver",
|
||||
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin Konto",
|
||||
"Repeat Password": "Wiederhole das Passwort",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
respTime: "Antw. Zeit (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/en.js
Normal file
112
src/languages/en.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "English",
|
||||
checkEverySecond: "Check every {0} seconds.",
|
||||
"Avg.": "Avg. ",
|
||||
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
|
||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
||||
passwordNotMatchMsg: "The repeat password does not match.",
|
||||
notificationDescription: "Please assign a notification to monitor(s) to get it to work.",
|
||||
keywordDescription: "Search keyword in plain html or JSON response and it is case-sensitive",
|
||||
pauseDashboardHome: "Pause",
|
||||
deleteMonitorMsg: "Are you sure want to delete this monitor?",
|
||||
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
|
||||
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.",
|
||||
rrtypeDescription: "Select the RR-Type you want to monitor",
|
||||
pauseMonitorMsg: "Are you sure want to pause?",
|
||||
Settings: "Settings",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "New Update",
|
||||
Language: "Language",
|
||||
Appearance: "Appearance",
|
||||
Theme: "Theme",
|
||||
General: "General",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Check Update On GitHub",
|
||||
List: "List",
|
||||
Add: "Add",
|
||||
"Add New Monitor": "Add New Monitor",
|
||||
"Quick Stats": "Quick Stats",
|
||||
Up: "Up",
|
||||
Down: "Down",
|
||||
Pending: "Pending",
|
||||
Unknown: "Unknown",
|
||||
Pause: "Pause",
|
||||
Name: "Name",
|
||||
Status: "Status",
|
||||
DateTime: "DateTime",
|
||||
Message: "Message",
|
||||
"No important events": "No important events",
|
||||
Resume: "Resume",
|
||||
Edit: "Edit",
|
||||
Delete: "Delete",
|
||||
Current: "Current",
|
||||
Uptime: "Uptime",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
days: "days",
|
||||
day: "day",
|
||||
"-day": "-day",
|
||||
hour: "hour",
|
||||
"-hour": "-hour",
|
||||
Response: "Response",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Monitor Type",
|
||||
Keyword: "Keyword",
|
||||
"Friendly Name": "Friendly Name",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Heartbeat Interval",
|
||||
Retries: "Retries",
|
||||
Advanced: "Advanced",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Max. Redirects": "Max. Redirects",
|
||||
"Accepted Status Codes": "Accepted Status Codes",
|
||||
Save: "Save",
|
||||
Notifications: "Notifications",
|
||||
"Not available, please setup.": "Not available, please setup.",
|
||||
"Setup Notification": "Setup Notification",
|
||||
Light: "Light",
|
||||
Dark: "Dark",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||
Normal: "Normal",
|
||||
Bottom: "Bottom",
|
||||
None: "None",
|
||||
Timezone: "Timezone",
|
||||
"Search Engine Visibility": "Search Engine Visibility",
|
||||
"Allow indexing": "Allow indexing",
|
||||
"Discourage search engines from indexing site": "Discourage search engines from indexing site",
|
||||
"Change Password": "Change Password",
|
||||
"Current Password": "Current Password",
|
||||
"New Password": "New Password",
|
||||
"Repeat New Password": "Repeat New Password",
|
||||
"Update Password": "Update Password",
|
||||
"Disable Auth": "Disable Auth",
|
||||
"Enable Auth": "Enable Auth",
|
||||
Logout: "Logout",
|
||||
Leave: "Leave",
|
||||
"I understand, please disable": "I understand, please disable",
|
||||
Confirm: "Confirm",
|
||||
Yes: "Yes",
|
||||
No: "No",
|
||||
Username: "Username",
|
||||
Password: "Password",
|
||||
"Remember me": "Remember me",
|
||||
Login: "Login",
|
||||
"No Monitors, please": "No Monitors, please",
|
||||
"add one": "add one",
|
||||
"Notification Type": "Notification Type",
|
||||
Email: "Email",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Certificate Info",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Last Result": "Last Result",
|
||||
"Create your admin account": "Create your admin account",
|
||||
"Repeat Password": "Repeat Password",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/es-ES.js
Normal file
112
src/languages/es-ES.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Español",
|
||||
checkEverySecond: "Comprobar cada {0} segundos.",
|
||||
"Avg.": "Media. ",
|
||||
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
|
||||
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
|
||||
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
|
||||
maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.",
|
||||
acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.",
|
||||
passwordNotMatchMsg: "La contraseña repetida no coincide.",
|
||||
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
|
||||
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
|
||||
pauseDashboardHome: "Pausar",
|
||||
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
|
||||
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
|
||||
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
|
||||
rrtypeDescription: "Selecciona el tipo de registro que quieres monitorizar",
|
||||
pauseMonitorMsg: "¿Seguro que quieres pausar?",
|
||||
Settings: "Ajustes",
|
||||
Dashboard: "Panel",
|
||||
"New Update": "Vueva actualización",
|
||||
Language: "Idioma",
|
||||
Appearance: "Apariencia",
|
||||
Theme: "Tema",
|
||||
General: "General",
|
||||
Version: "Versión",
|
||||
"Check Update On GitHub": "Comprobar actualizaciones en GitHub",
|
||||
List: "Lista",
|
||||
Add: "Añadir",
|
||||
"Add New Monitor": "Añadir nuevo monitor",
|
||||
"Quick Stats": "Estadísticas rápidas",
|
||||
Up: "Funcional",
|
||||
Down: "Caído",
|
||||
Pending: "Pendiente",
|
||||
Unknown: "Desconociso",
|
||||
Pause: "Pausa",
|
||||
Name: "Nombre",
|
||||
Status: "Estado",
|
||||
DateTime: "Fecha y Hora",
|
||||
Message: "Mensaje",
|
||||
"No important events": "No hay eventos importantes",
|
||||
Resume: "Reanudar",
|
||||
Edit: "Editar",
|
||||
Delete: "Eliminar",
|
||||
Current: "Actual",
|
||||
Uptime: "Tiempo activo",
|
||||
"Cert Exp.": "Caducidad cert.",
|
||||
days: "días",
|
||||
day: "día",
|
||||
"-day": "-día",
|
||||
hour: "hora",
|
||||
"-hour": "-hora",
|
||||
Response: "Respuesta",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Tipo de Monitor",
|
||||
Keyword: "Palabra clave",
|
||||
"Friendly Name": "Nombre sencillo",
|
||||
URL: "URL",
|
||||
Hostname: "Nombre del host",
|
||||
Port: "Puerto",
|
||||
"Heartbeat Interval": "Intervalo de latido",
|
||||
Retries: "Reintentos",
|
||||
Advanced: "Avanzado",
|
||||
"Upside Down Mode": "Modo invertido",
|
||||
"Max. Redirects": "Máx. redirecciones",
|
||||
"Accepted Status Codes": "Códigos de estado aceptados",
|
||||
Save: "Guardar",
|
||||
Notifications: "Notificaciones",
|
||||
"Not available, please setup.": "No disponible, por favor configurar.",
|
||||
"Setup Notification": "Configurar notificación",
|
||||
Light: "Claro",
|
||||
Dark: "Oscuro",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Tema - Barra de intervalo de latido",
|
||||
Normal: "Normal",
|
||||
Bottom: "Abajo",
|
||||
None: "Ninguno",
|
||||
Timezone: "Zona horaria",
|
||||
"Search Engine Visibility": "Visibilidad motor de búsqueda",
|
||||
"Allow indexing": "Permitir indexación",
|
||||
"Discourage search engines from indexing site": "Disuadir a los motores de búsqueda de indexar el sitio",
|
||||
"Change Password": "Cambiar contraseña",
|
||||
"Current Password": "Contraseña actual",
|
||||
"New Password": "Nueva contraseña",
|
||||
"Repeat New Password": "Repetir nueva contraseña",
|
||||
"Update Password": "Actualizar contraseña",
|
||||
"Disable Auth": "Deshabilitar Autenticación ",
|
||||
"Enable Auth": "Habilitar Autenticación ",
|
||||
Logout: "Cerrar sesión",
|
||||
Leave: "Salir",
|
||||
"I understand, please disable": "Lo comprendo, por favor deshabilitar",
|
||||
Confirm: "Confirmar",
|
||||
Yes: "Sí",
|
||||
No: "No",
|
||||
Username: "Usuario",
|
||||
Password: "Contraseña",
|
||||
"Remember me": "Recordarme",
|
||||
Login: "Acceso",
|
||||
"No Monitors, please": "Sin monitores, por favor",
|
||||
"add one": "añade uno",
|
||||
"Notification Type": "Tipo de notificación",
|
||||
Email: "Email",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Información del certificado ",
|
||||
"Resolver Server": "Servidor de resolución",
|
||||
"Resource Record Type": "Tipo de Registro",
|
||||
"Last Result": "Último resultado",
|
||||
"Create your admin account": "Crea tu cuenta de administrador",
|
||||
"Repeat Password": "Repetir contraseña",
|
||||
respTime: "Tiempo de resp. (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/fr.js
Normal file
112
src/languages/fr.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Français (France)",
|
||||
Settings: "Paramètres",
|
||||
Dashboard: "Tableau de bord",
|
||||
"New Update": "Mise à jour disponible",
|
||||
Language: "Langue",
|
||||
Appearance: "Apparence",
|
||||
Theme: "Thème",
|
||||
General: "Général",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Consulter les mises à jour sur Github",
|
||||
List: "Lister",
|
||||
Add: "Ajouter",
|
||||
"Add New Monitor": "Ajouter une nouvelle sonde",
|
||||
"Quick Stats": "Résumé",
|
||||
Up: "En ligne",
|
||||
Down: "Hors ligne",
|
||||
Pending: "En attente",
|
||||
Unknown: "Inconnu",
|
||||
Pause: "En Pause",
|
||||
pauseDashboardHome: "Éléments mis en pause",
|
||||
Name: "Nom",
|
||||
Status: "État",
|
||||
DateTime: "Heure",
|
||||
Message: "Messages",
|
||||
"No important events": "Pas d'évènements important",
|
||||
Resume: "Reprendre",
|
||||
Edit: "Modifier",
|
||||
Delete: "Supprimer",
|
||||
Current: "Actuellement",
|
||||
Uptime: "Uptime",
|
||||
"Cert Exp.": "Certificat expiré",
|
||||
days: "Jours",
|
||||
day: "Jour",
|
||||
"-day": "Journée",
|
||||
hour: "Heure",
|
||||
"-hour": "Heures",
|
||||
checkEverySecond: "Vérifier toutes les {0} secondes",
|
||||
"Avg.": "Moyen",
|
||||
Response: "Temps de réponse",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Type de Sonde",
|
||||
Keyword: "Mot-clé",
|
||||
"Friendly Name": "Nom d'affichage",
|
||||
URL: "URL",
|
||||
Hostname: "Nom d'hôte",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Intervale de vérification",
|
||||
Retries: "Essais",
|
||||
retriesDescription: "Nombre d'essais avant que le service soit déclaré hors-ligne.",
|
||||
Advanced: "Avancé",
|
||||
ignoreTLSError: "Ignorer les erreurs liées au certificat SSL/TLS",
|
||||
"Upside Down Mode": "Mode inversé",
|
||||
upsideDownModeDescription: "Si le service est en ligne, il sera alors noté hors-ligne et vice-versa.",
|
||||
"Max. Redirects": "Nombre maximum de redirections",
|
||||
maxRedirectDescription: "Nombre maximal de redirections avant que le service soit noté hors-ligne.",
|
||||
"Accepted Status Codes": "Codes HTTP",
|
||||
acceptedStatusCodesDescription: "Codes HTTP considérés comme en ligne",
|
||||
Save: "Sauvegarder",
|
||||
Notifications: "Notifications",
|
||||
"Not available, please setup.": "Pas de système de notification disponible, merci de le configurer",
|
||||
"Setup Notification": "Créer une notification",
|
||||
Light: "Clair",
|
||||
Dark: "Sombre",
|
||||
Auto: "Automatique",
|
||||
"Theme - Heartbeat Bar": "Voir les services surveillés",
|
||||
Normal: "Général",
|
||||
Bottom: "En dessous",
|
||||
None: "Non",
|
||||
Timezone: "Fuseau Horaire",
|
||||
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
|
||||
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
|
||||
"Discourage search engines from indexing site": "Refuser l'indexation par des moteurs de recherche",
|
||||
"Change Password": "Changer le mot de passe",
|
||||
"Current Password": "Mot de passe actuel",
|
||||
"New Password": "Nouveau mot de passe",
|
||||
"Repeat New Password": "Répéter votre nouveau mot de passe",
|
||||
passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
|
||||
"Update Password": "Mettre à jour le mot de passe",
|
||||
"Disable Auth": "Désactiver l'authentification",
|
||||
"Enable Auth": "Activer l'authentification",
|
||||
Logout: "Se déconnecter",
|
||||
notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.",
|
||||
Leave: "Quitter",
|
||||
"I understand, please disable": "J'ai compris, désactivez-le",
|
||||
Confirm: "Confirmer",
|
||||
Yes: "Oui",
|
||||
No: "Non",
|
||||
Username: "Nom d'utilisateur",
|
||||
Password: "Mot de passe",
|
||||
"Remember me": "Se souvenir de moi",
|
||||
Login: "Se connecter",
|
||||
"No Monitors, please": "Pas de sondes, veuillez ",
|
||||
"add one": "en ajouter une.",
|
||||
"Notification Type": "Type de notification",
|
||||
Email: "Email",
|
||||
Test: "Tester",
|
||||
keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.",
|
||||
"Certificate Info": "Informations sur le certificat SSL",
|
||||
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
|
||||
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
|
||||
"Resolver Server": "Serveur DNS utilisé",
|
||||
"Resource Record Type": "Type d'enregistrement DNS recherché",
|
||||
resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
|
||||
rrtypeDescription: "Veuillez séléctionner un type d'enregistrement DNS",
|
||||
pauseMonitorMsg: "Etes vous sur de vouloir mettre en pause cette sonde ?",
|
||||
"Last Result": "Dernier résultat",
|
||||
"Create your admin account": "Créez votre compte administrateur",
|
||||
"Repeat Password": "Répéter le mot de passe",
|
||||
respTime: "Temps de réponse (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/ja.js
Normal file
112
src/languages/ja.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "日本語",
|
||||
checkEverySecond: "{0}秒ごとにチェックします。",
|
||||
"Avg.": "平均 ",
|
||||
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
|
||||
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
|
||||
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
|
||||
maxRedirectDescription: "フォローするリダイレクトの最大数。リダイレクトを無効にするには0を設定する。",
|
||||
acceptedStatusCodesDescription: "成功した応答とみなされるステータスコードを選択する。",
|
||||
passwordNotMatchMsg: "繰り返しのパスワードが一致しません。",
|
||||
notificationDescription: "監視を機能させるには、監視に通知を割り当ててください。",
|
||||
keywordDescription: "プレーンHTMLまたはJSON応答でキーワードを検索し、大文字と小文字を区別します",
|
||||
pauseDashboardHome: "一時停止",
|
||||
deleteMonitorMsg: "この監視を削除してよろしいですか?",
|
||||
deleteNotificationMsg: "全ての監視のこの通知を削除してよろしいですか?",
|
||||
resoverserverDescription: "Cloudflareがデフォルトのサーバーですが、いつでもリゾルバサーバーを変更できます。",
|
||||
rrtypeDescription: "監視するRRタイプを選択します",
|
||||
pauseMonitorMsg: "一時停止しますか?",
|
||||
Settings: "設定",
|
||||
Dashboard: "ダッシュボード",
|
||||
"New Update": "New Update",
|
||||
Language: "言語",
|
||||
Appearance: "外観",
|
||||
Theme: "テーマ",
|
||||
General: "General",
|
||||
Version: "バージョン",
|
||||
"Check Update On GitHub": "GitHubでアップデートを確認する",
|
||||
List: "一覧",
|
||||
Add: "追加",
|
||||
"Add New Monitor": "監視の追加",
|
||||
"Quick Stats": "統計",
|
||||
Up: "Up",
|
||||
Down: "Down",
|
||||
Pending: "中止",
|
||||
Unknown: "不明",
|
||||
Pause: "一時停止",
|
||||
Name: "名前",
|
||||
Status: "ステータス",
|
||||
DateTime: "日時",
|
||||
Message: "メッセージ",
|
||||
"No important events": "重要なイベントなし",
|
||||
Resume: "再開",
|
||||
Edit: "編集",
|
||||
Delete: "削除",
|
||||
Current: "現在",
|
||||
Uptime: "起動時間",
|
||||
"Cert Exp.": "証明書有効期限",
|
||||
days: "日間",
|
||||
day: "日",
|
||||
"-day": "-日",
|
||||
hour: "時間",
|
||||
"-hour": "-時間",
|
||||
Response: "レスポンス",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "監視タイプ",
|
||||
Keyword: "キーワード",
|
||||
"Friendly Name": "Friendly Name",
|
||||
URL: "URL",
|
||||
Hostname: "ホスト名",
|
||||
Port: "ポート",
|
||||
"Heartbeat Interval": "監視間隔",
|
||||
Retries: "Retries",
|
||||
Advanced: "Advanced",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Max. Redirects": "最大リダイレクト数",
|
||||
"Accepted Status Codes": "承認されたステータスコード",
|
||||
Save: "保存",
|
||||
Notifications: "通知",
|
||||
"Not available, please setup.": "利用できません。設定してください。",
|
||||
"Setup Notification": "通知設定",
|
||||
Light: "Light",
|
||||
Dark: "Dark",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||
Normal: "通常",
|
||||
Bottom: "下部",
|
||||
None: "なし",
|
||||
Timezone: "タイムゾーン",
|
||||
"Search Engine Visibility": "検索エンジンでの表示",
|
||||
"Allow indexing": "インデックス作成を許可する",
|
||||
"Discourage search engines from indexing site": "検索エンジンにインデックスさせないようにする",
|
||||
"Change Password": "パスワード変更",
|
||||
"Current Password": "現在のパスワード",
|
||||
"New Password": "新しいパスワード",
|
||||
"Repeat New Password": "確認のため新しいパスワードをもう一度",
|
||||
"Update Password": "パスワードの更新",
|
||||
"Disable Auth": "認証の無効化",
|
||||
"Enable Auth": "認証の有効化",
|
||||
Logout: "ログアウト",
|
||||
Leave: "作業を中止する",
|
||||
"I understand, please disable": "理解した上で無効化する",
|
||||
Confirm: "確認",
|
||||
Yes: "はい",
|
||||
No: "いいえ",
|
||||
Username: "ユーザー名",
|
||||
Password: "パスワード",
|
||||
"Remember me": "パスワードを忘れた場合",
|
||||
Login: "ログイン",
|
||||
"No Monitors, please": "監視がありません",
|
||||
"add one": "add one",
|
||||
"Notification Type": "通知タイプ",
|
||||
Email: "Eメール",
|
||||
Test: "テスト",
|
||||
"Certificate Info": "証明書情報",
|
||||
"Resolver Server": "問い合わせ先DNSサーバ",
|
||||
"Resource Record Type": "DNSレコード設定",
|
||||
"Last Result": "最終結果",
|
||||
"Create your admin account": "Create your admin account",
|
||||
"Repeat Password": "Repeat Password",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/ko-KR.js
Normal file
112
src/languages/ko-KR.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "한국어",
|
||||
checkEverySecond: "{0} 초마다 체크해요.",
|
||||
"Avg.": "평균 ",
|
||||
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
|
||||
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
|
||||
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
|
||||
maxRedirectDescription: "최대 리다이렉트 횟수에요. 0을 입력하면 리다이렉트를 꺼요.",
|
||||
acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.",
|
||||
passwordNotMatchMsg: "비밀번호 재입력이 일치하지 않아요.",
|
||||
notificationDescription: "모니터링에 알림을 설정할 수 있어요.",
|
||||
keywordDescription: "Html 이나 JSON에서 대소문자를 구분해 키워드를 검색해요.",
|
||||
pauseDashboardHome: "일시 정지",
|
||||
deleteMonitorMsg: "정말 이 모니터링을 삭제할까요?",
|
||||
deleteNotificationMsg: "정말 이 알림을 모든 모니터링에서 삭제할까요?",
|
||||
resoverserverDescription: "Cloudflare가 기본 서버에요, 원한다면 언제나 다른 resolver 서버로 변경할 수 있어요.",
|
||||
rrtypeDescription: "모니터링할 RR-Type을 선택해요.",
|
||||
pauseMonitorMsg: "정말 이 모니터링을 일시 정지 할까요?",
|
||||
Settings: "설정",
|
||||
Dashboard: "대시보드",
|
||||
"New Update": "새로운 업데이트",
|
||||
Language: "언어",
|
||||
Appearance: "외형",
|
||||
Theme: "테마",
|
||||
General: "일반",
|
||||
Version: "버전",
|
||||
"Check Update On GitHub": "깃허브에서 업데이트 확인",
|
||||
List: "목록",
|
||||
Add: "추가",
|
||||
"Add New Monitor": "새로운 모니터링 추가하기",
|
||||
"Quick Stats": "간단한 정보",
|
||||
Up: "온라인",
|
||||
Down: "오프라인",
|
||||
Pending: "대기 중",
|
||||
Unknown: "알 수 없음",
|
||||
Pause: "일시 정지",
|
||||
Name: "이름",
|
||||
Status: "상태",
|
||||
DateTime: "날짜",
|
||||
Message: "메시지",
|
||||
"No important events": "중요 이벤트 없음",
|
||||
Resume: "재개",
|
||||
Edit: "수정",
|
||||
Delete: "삭제",
|
||||
Current: "현재",
|
||||
Uptime: "업타임",
|
||||
"Cert Exp.": "인증서 만료",
|
||||
days: "일",
|
||||
day: "일",
|
||||
"-day": "-일",
|
||||
hour: "시간",
|
||||
"-hour": "-시간",
|
||||
Response: "응답",
|
||||
Ping: "핑",
|
||||
"Monitor Type": "모니터링 종류",
|
||||
Keyword: "키워드",
|
||||
"Friendly Name": "이름",
|
||||
URL: "URL",
|
||||
Hostname: "호스트네임",
|
||||
Port: "포트",
|
||||
"Heartbeat Interval": "하트비트 주기",
|
||||
Retries: "재시도",
|
||||
Advanced: "고급",
|
||||
"Upside Down Mode": "상태 반전 모드",
|
||||
"Max. Redirects": "최대 리다이렉트",
|
||||
"Accepted Status Codes": "응답 성공 상태 코드",
|
||||
Save: "저장",
|
||||
Notifications: "알림",
|
||||
"Not available, please setup.": "존재하지 않아요, 새로운거 하나 만드는건 어때요?",
|
||||
"Setup Notification": "알림 설정",
|
||||
Light: "라이트",
|
||||
Dark: "다크",
|
||||
Auto: "자동",
|
||||
"Theme - Heartbeat Bar": "테마 - 하트비트 바",
|
||||
Normal: "기본값",
|
||||
Bottom: "가운데",
|
||||
None: "제거",
|
||||
Timezone: "시간대",
|
||||
"Search Engine Visibility": "검색 엔진 활성화",
|
||||
"Allow indexing": "인덱싱 허용",
|
||||
"Discourage search engines from indexing site": "검색 엔진 인덱싱 거부",
|
||||
"Change Password": "비밀번호 변경",
|
||||
"Current Password": "기존 비밀번호",
|
||||
"New Password": "새로운 비밀번호",
|
||||
"Repeat New Password": "새로운 비밀번호 재입력",
|
||||
"Update Password": "비밀번호 변경",
|
||||
"Disable Auth": "인증 끄기",
|
||||
"Enable Auth": "인증 켜기",
|
||||
Logout: "로그아웃",
|
||||
Leave: "나가기",
|
||||
"I understand, please disable": "기능에 대해 이해했으니 꺼주세요.",
|
||||
Confirm: "확인",
|
||||
Yes: "확인",
|
||||
No: "취소",
|
||||
Username: "이름",
|
||||
Password: "비밀번호",
|
||||
"Remember me": "비밀번호 기억하기",
|
||||
Login: "로그인",
|
||||
"No Monitors, please": "모니터링이 없어요,",
|
||||
"add one": "하나 추가해봐요",
|
||||
"Notification Type": "알림 종류",
|
||||
Email: "이메일",
|
||||
Test: "테스트",
|
||||
"Certificate Info": "인증서 정보",
|
||||
"Resolver Server": "Resolver 서버",
|
||||
"Resource Record Type": "자원 레코드 유형",
|
||||
"Last Result": "최근 결과",
|
||||
"Create your admin account": "관리자 계정 만들기",
|
||||
"Repeat Password": "비밀번호 재입력",
|
||||
respTime: "응답 시간 (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/nl-NL.js
Normal file
112
src/languages/nl-NL.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Nederlands",
|
||||
checkEverySecond: "Controleer elke {0} seconden.",
|
||||
"Avg.": "Gem. ",
|
||||
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
|
||||
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
|
||||
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
|
||||
maxRedirectDescription: "Maximaal aantal te volgen omleidingen. Stel in op 0 om omleidingen uit te schakelen.",
|
||||
acceptedStatusCodesDescription: "Selecteer statuscodes die als een succesvol antwoord worden beschouwd.",
|
||||
passwordNotMatchMsg: "Het herhaalwachtwoord komt niet overeen.",
|
||||
notificationDescription: "Wijs a.u.b. een melding toe aan de monitor(s) om het te laten werken.",
|
||||
keywordDescription: "Zoek trefwoord in gewone html of JSON-response en het is hoofdlettergevoelig",
|
||||
pauseDashboardHome: "Gepauzeerd",
|
||||
deleteMonitorMsg: "Weet u zeker dat u deze monitor wilt verwijderen?",
|
||||
deleteNotificationMsg: "Weet u zeker dat u deze melding voor alle monitoren wilt verwijderen?",
|
||||
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
|
||||
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
|
||||
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
|
||||
Settings: "Instellingen",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Nieuwe update",
|
||||
Language: "Taal",
|
||||
Appearance: "Weergave",
|
||||
Theme: "Thema",
|
||||
General: "Algemeen",
|
||||
Version: "Versie",
|
||||
"Check Update On GitHub": "Controleer voor updates op GitHub",
|
||||
List: "Lijst",
|
||||
Add: "Toevoegen",
|
||||
"Add New Monitor": "Nieuwe monitor toevoegen",
|
||||
"Quick Stats": "Snelle statistieken",
|
||||
Up: "Online",
|
||||
Down: "Offline",
|
||||
Pending: "In afwachting",
|
||||
Unknown: "Onbekend",
|
||||
Pause: "Pauze",
|
||||
Name: "Naam",
|
||||
Status: "Status",
|
||||
DateTime: "Datum Tijd",
|
||||
Message: "Bericht",
|
||||
"No important events": "Geen belangrijke gebeurtenissen",
|
||||
Resume: "Hervat",
|
||||
Edit: "Wijzigen",
|
||||
Delete: "Verwijderen",
|
||||
Current: "Huidig",
|
||||
Uptime: "Uptime",
|
||||
"Cert Exp.": "Cert. verl.",
|
||||
days: "dagen",
|
||||
day: "dag",
|
||||
"-day": "-dag",
|
||||
hour: "uur",
|
||||
"-hour": "-uur",
|
||||
Response: "Antwoord",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Monitortype:",
|
||||
Keyword: "Trefwoord",
|
||||
"Friendly Name": "Vriendelijke naam",
|
||||
URL: "URL",
|
||||
Hostname: "Hostnaam",
|
||||
Port: "Poort",
|
||||
"Heartbeat Interval": "Hartslaginterval",
|
||||
Retries: "Pogingen",
|
||||
Advanced: "Geavanceerd",
|
||||
"Upside Down Mode": "Ondersteboven modus",
|
||||
"Max. Redirects": "Max. Omleidingen",
|
||||
"Accepted Status Codes": "Geaccepteerde statuscodes",
|
||||
Save: "Opslaan",
|
||||
Notifications: "Meldingen",
|
||||
"Not available, please setup.": "Niet beschikbaar, stel a.u.b. in.",
|
||||
"Setup Notification": "Melding instellen",
|
||||
Light: "Licht",
|
||||
Dark: "Donker",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Thema - Hartslagbalk",
|
||||
Normal: "Normaal",
|
||||
Bottom: "Onderkant",
|
||||
None: "Geen",
|
||||
Timezone: "Tijdzone",
|
||||
"Search Engine Visibility": "Zichtbaarheid voor zoekmachines",
|
||||
"Allow indexing": "Indexering toestaan",
|
||||
"Discourage search engines from indexing site": "Ontmoedig zoekmachines om de site te indexeren",
|
||||
"Change Password": "Verander wachtwoord",
|
||||
"Current Password": "Huidig wachtwoord",
|
||||
"New Password": "Nieuw wachtwoord",
|
||||
"Repeat New Password": "Herhaal nieuw wachtwoord",
|
||||
"Update Password": "Vernieuw wachtwoord",
|
||||
"Disable Auth": "Autorisatie uitschakelen",
|
||||
"Enable Auth": "Autorisatie inschakelen",
|
||||
Logout: "Uitloggen",
|
||||
Leave: "Vertrekken",
|
||||
"I understand, please disable": "Ik begrijp het, schakel a.u.b. uit",
|
||||
Confirm: "Bevestigen",
|
||||
Yes: "Ja",
|
||||
No: "Nee",
|
||||
Username: "Gebruikersnaam",
|
||||
Password: "Wachtwoord",
|
||||
"Remember me": "Wachtwoord onthouden",
|
||||
Login: "Inloggen",
|
||||
"No Monitors, please": "Geen monitoren, ",
|
||||
"add one": "voeg een toe",
|
||||
"Notification Type": "Melding type",
|
||||
Email: "E-mail",
|
||||
Test: "Testen",
|
||||
"Certificate Info": "Certificaat informatie",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Resource Record Type": "Type bronrecord",
|
||||
"Last Result": "Laatste resultaat",
|
||||
"Create your admin account": "Maak uw beheerdersaccount aan",
|
||||
"Repeat Password": "Herhaal wachtwoord",
|
||||
respTime: "resp. tijd (ms)",
|
||||
notAvailableShort: "N.v.t."
|
||||
}
|
112
src/languages/ru-RU.js
Normal file
112
src/languages/ru-RU.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Русский",
|
||||
checkEverySecond: "Проверять каждые {0} секунд.",
|
||||
"Avg.": "Средн. ",
|
||||
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
|
||||
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
|
||||
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
|
||||
maxRedirectDescription: "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
|
||||
acceptedStatusCodesDescription: "Выберите коды статусов, которые должны считаться за успешный ответ.",
|
||||
passwordNotMatchMsg: "Повтор пароля не совпадает.",
|
||||
notificationDescription: "Привяжите уведомления к мониторам.",
|
||||
keywordDescription: "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру)",
|
||||
pauseDashboardHome: "Пауза",
|
||||
deleteMonitorMsg: "Вы действительно хотите удалить данный монитор?",
|
||||
deleteNotificationMsg: "Вы действительно хотите удалить это уведомление для всех мониторов?",
|
||||
resoverserverDescription: "Cloudflare является сервером по умолчанию. Вы всегда можете сменить данный сервер.",
|
||||
rrtypeDescription: "Выберите тип ресурсной записи, который вы хотите отслеживать",
|
||||
pauseMonitorMsg: "Вы действительно хотите поставить на паузу?",
|
||||
Settings: "Настройки",
|
||||
Dashboard: "Панель",
|
||||
"New Update": "Обновление",
|
||||
Language: "Язык",
|
||||
Appearance: "Внешний вид",
|
||||
Theme: "Тема",
|
||||
General: "Общее",
|
||||
Version: "Версия",
|
||||
"Check Update On GitHub": "Проверить обновления на GitHub",
|
||||
List: "Список",
|
||||
Add: "Добавить",
|
||||
"Add New Monitor": "Новый монитор",
|
||||
"Quick Stats": "Статистика",
|
||||
Up: "Доступно",
|
||||
Down: "Недоступно",
|
||||
Pending: "Ожидание",
|
||||
Unknown: "Неизвестно",
|
||||
Pause: "Пауза",
|
||||
Name: "Имя",
|
||||
Status: "Статус",
|
||||
DateTime: "Дата и время",
|
||||
Message: "Сообщение",
|
||||
"No important events": "Важных событий нет",
|
||||
Resume: "Возобновить",
|
||||
Edit: "Изменить",
|
||||
Delete: "Удалить",
|
||||
Current: "Текущий",
|
||||
Uptime: "Аптайм",
|
||||
"Cert Exp.": "Сертификат просрочен",
|
||||
days: "дней",
|
||||
day: "день",
|
||||
"-day": " дней",
|
||||
hour: "час",
|
||||
"-hour": " часа",
|
||||
Response: "Ответ",
|
||||
Ping: "Пинг",
|
||||
"Monitor Type": "Тип монитора",
|
||||
Keyword: "Слово",
|
||||
"Friendly Name": "Имя",
|
||||
URL: "URL",
|
||||
Hostname: "Имя хоста",
|
||||
Port: "Порт",
|
||||
"Heartbeat Interval": "Частота опроса",
|
||||
Retries: "Попыток",
|
||||
Advanced: "Дополнительно",
|
||||
"Upside Down Mode": "Режим реверса статуса",
|
||||
"Max. Redirects": "Макс. перенаправлений",
|
||||
"Accepted Status Codes": "Допустимые коды статуса",
|
||||
Save: "Сохранить",
|
||||
Notifications: "Уведомления",
|
||||
"Not available, please setup.": "Доступных уведомлений нет, необходима настройка.",
|
||||
"Setup Notification": "Настроить уведомления",
|
||||
Light: "Светлая",
|
||||
Dark: "Тёмная",
|
||||
Auto: "Авто",
|
||||
"Theme - Heartbeat Bar": "Тема - Полоса частоты опроса",
|
||||
Normal: "Обычный",
|
||||
Bottom: "Снизу",
|
||||
None: "Отсутствует",
|
||||
Timezone: "Часовой пояс",
|
||||
"Search Engine Visibility": "Видимость поисковым движком",
|
||||
"Allow indexing": "Разрешить индексирование",
|
||||
"Discourage search engines from indexing site": "Не позволять индексировать сайт",
|
||||
"Change Password": "Сменить пароль",
|
||||
"Current Password": "Текущий пароль",
|
||||
"New Password": "Новый пароль",
|
||||
"Repeat New Password": "Повтор нового пароля",
|
||||
"Update Password": "Обновить пароль",
|
||||
"Disable Auth": "Отключить авторизацию",
|
||||
"Enable Auth": "Включить авторизацию",
|
||||
Logout: "Выйти",
|
||||
Leave: "Отмена",
|
||||
"I understand, please disable": "Я понимаю, всё равно отключить",
|
||||
Confirm: "Подтвердить",
|
||||
Yes: "Да",
|
||||
No: "Нет",
|
||||
Username: "Логин",
|
||||
Password: "Пароль",
|
||||
"Remember me": "Запомнить меня",
|
||||
Login: "Вход в систему",
|
||||
"No Monitors, please": "Мониторов нет, пожалуйста",
|
||||
"add one": "создайте новый",
|
||||
"Notification Type": "Тип уведомления",
|
||||
Email: "Почта",
|
||||
Test: "Проверка",
|
||||
"Certificate Info": "Информация о сертификате",
|
||||
"Resolver Server": "DNS сервер",
|
||||
"Resource Record Type": "Тип ресурсной записи",
|
||||
"Last Result": "Последний результат",
|
||||
"Create your admin account": "Создайте аккаунт администратора",
|
||||
"Repeat Password": "Повторите пароль",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/sr-latn.js
Normal file
112
src/languages/sr-latn.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Srpski",
|
||||
checkEverySecond: "Proveri svakih {0} sekundi.",
|
||||
"Avg.": "Prosečni ",
|
||||
retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
|
||||
ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
|
||||
upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",
|
||||
maxRedirectDescription: "Maksimani broj preusmerenja da se prate. Postavite na 0 da bi se isključila preusmerenja.",
|
||||
acceptedStatusCodesDescription: "Odaberite statusne kodove koji se smatraju uspešnim odgovorom.",
|
||||
passwordNotMatchMsg: "Ponovljena lozinka se ne poklapa.",
|
||||
notificationDescription: "Molim Vas postavite obaveštenje za masmatrače da bise aktivirali.",
|
||||
keywordDescription: "Pretraži ključnu reč u čistom html ili JSON odgovoru sa osetljivim velikim i malim slovima",
|
||||
pauseDashboardHome: "Pauziraj",
|
||||
deleteMonitorMsg: "Da li ste sigurni da želite da obrišete ovog posmatrača?",
|
||||
deleteNotificationMsg: "Da li ste sigurni d aželite da uklonite ovo obaveštenje za sve posmatrače?",
|
||||
resoverserverDescription: "Cloudflare je podrazumevani server. Možete promeniti server za raszrešavanje u bilo kom trenutku.",
|
||||
rrtypeDescription: "Odaberite RR-Type koji želite da posmatrate",
|
||||
pauseMonitorMsg: "Da li ste sigurni da želite da pauzirate?",
|
||||
Settings: "Podešavanja",
|
||||
Dashboard: "Komandna tabla",
|
||||
"New Update": "Nova verzija",
|
||||
Language: "Jezik",
|
||||
Appearance: "Izgled",
|
||||
Theme: "Tema",
|
||||
General: "Opšte",
|
||||
Version: "Verzija",
|
||||
"Check Update On GitHub": "Proverite novu verziju na GitHub-u",
|
||||
List: "Lista",
|
||||
Add: "Dodaj",
|
||||
"Add New Monitor": "Dodaj novog posmatrača",
|
||||
"Quick Stats": "Brze statistike",
|
||||
Up: "Aktivno",
|
||||
Down: "Neaktivno",
|
||||
Pending: "Nerešeno",
|
||||
Unknown: "Nepoznato",
|
||||
Pause: "Pauziraj",
|
||||
Name: "Ime",
|
||||
Status: "Status",
|
||||
DateTime: "Datum i vreme",
|
||||
Message: "Poruka",
|
||||
"No important events": "Nema bitnih događaja",
|
||||
Resume: "Nastavi",
|
||||
Edit: "Izmeni",
|
||||
Delete: "Ukloni",
|
||||
Current: "Trenutno",
|
||||
Uptime: "Vreme rada",
|
||||
"Cert Exp.": "Istek sert.",
|
||||
days: "dana",
|
||||
day: "dan",
|
||||
"-day": "-dana",
|
||||
hour: "sat",
|
||||
"-hour": "-sata",
|
||||
Response: "Odgovor",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Tip posmatrača",
|
||||
Keyword: "Ključna reč",
|
||||
"Friendly Name": "Prijateljsko ime",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Interval otkucaja srca",
|
||||
Retries: "Pokušaji",
|
||||
Advanced: "Napredno",
|
||||
"Upside Down Mode": "Naopak mod",
|
||||
"Max. Redirects": "Maks. preusmerenja",
|
||||
"Accepted Status Codes": "Prihvaćeni statusni kodovi",
|
||||
Save: "Sačuvaj",
|
||||
Notifications: "Obaveštenja",
|
||||
"Not available, please setup.": "Nije dostupno, molim Vas podesite.",
|
||||
"Setup Notification": "Postavi obaveštenje",
|
||||
Light: "Svetlo",
|
||||
Dark: "Tamno",
|
||||
Auto: "Automatsko",
|
||||
"Theme - Heartbeat Bar": "Tema - Traka otkucaja srca",
|
||||
Normal: "Normalno",
|
||||
Bottom: "Dole",
|
||||
None: "Isključeno",
|
||||
Timezone: "Vremenska zona",
|
||||
"Search Engine Visibility": "Vidljivost pretraživačima",
|
||||
"Allow indexing": "Dozvoli indeksiranje",
|
||||
"Discourage search engines from indexing site": "Odvraćajte pretraživače od indeksiranja sajta",
|
||||
"Change Password": "Promeni lozinku",
|
||||
"Current Password": "Trenutna lozinka",
|
||||
"New Password": "Nova lozinka",
|
||||
"Repeat New Password": "Ponovi novu lozinku",
|
||||
"Update Password": "Izmeni lozinku",
|
||||
"Disable Auth": "Isključi autentifikaciju",
|
||||
"Enable Auth": "Uključi autentifikaciju",
|
||||
Logout: "Odloguj se",
|
||||
Leave: "Izađi",
|
||||
"I understand, please disable": "Razumem, molim te isključi",
|
||||
Confirm: "Potvrdi",
|
||||
Yes: "Da",
|
||||
No: "Ne",
|
||||
Username: "Korisničko ime",
|
||||
Password: "Lozinka",
|
||||
"Remember me": "Zapamti me",
|
||||
Login: "Uloguj se",
|
||||
"No Monitors, please": "Bez posmatrača molim",
|
||||
"add one": "dodaj jednog",
|
||||
"Notification Type": "Tip obaveštenja",
|
||||
Email: "E-pošta",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Informacije sertifikata",
|
||||
"Resolver Server": "Razrešivački server",
|
||||
"Resource Record Type": "Tip zapisa resursa",
|
||||
"Last Result": "Poslednji rezultat",
|
||||
"Create your admin account": "Naprivi administratorski nalog",
|
||||
"Repeat Password": "Ponovite lozinku",
|
||||
respTime: "Vreme odg. (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/sr.js
Normal file
112
src/languages/sr.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Српски",
|
||||
checkEverySecond: "Провери сваких {0} секунди.",
|
||||
"Avg.": "Просечни ",
|
||||
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
|
||||
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
|
||||
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
|
||||
maxRedirectDescription: "Максимани број преусмерења да се прате. Поставите на 0 да би се искључила преусмерења.",
|
||||
acceptedStatusCodesDescription: "Одаберите статусне кодове који се сматрају успешним одговором.",
|
||||
passwordNotMatchMsg: "Поновљена лозинка се не поклапа.",
|
||||
notificationDescription: "Молим Вас поставите обавештење за масматраче да бисе активирали.",
|
||||
keywordDescription: "Претражи кључну реч у чистом html или JSON одговору са осетљивим великим и малим словима",
|
||||
pauseDashboardHome: "Паузирај",
|
||||
deleteMonitorMsg: "Да ли сте сигурни да желите да обришете овог посматрача?",
|
||||
deleteNotificationMsg: "Да ли сте сигурни д ажелите да уклоните ово обавештење за све посматраче?",
|
||||
resoverserverDescription: "Cloudflare је подразумевани сервер. Можете променити сервер за расзрешавање у било ком тренутку.",
|
||||
rrtypeDescription: "Одаберите RR-Type који желите да посматрате",
|
||||
pauseMonitorMsg: "Да ли сте сигурни да желите да паузирате?",
|
||||
Settings: "Подешавања",
|
||||
Dashboard: "Командна табла",
|
||||
"New Update": "Нова верзија",
|
||||
Language: "Језик",
|
||||
Appearance: "Изглед",
|
||||
Theme: "Тема",
|
||||
General: "Опште",
|
||||
Version: "Верзија",
|
||||
"Check Update On GitHub": "Проверите нову верзију на GitHub-у",
|
||||
List: "Листа",
|
||||
Add: "Додај",
|
||||
"Add New Monitor": "Додај новог посматрача",
|
||||
"Quick Stats": "Брзе статистике",
|
||||
Up: "Активно",
|
||||
Down: "Неактивно",
|
||||
Pending: "Нерешено",
|
||||
Unknown: "Непознато",
|
||||
Pause: "Паузирај",
|
||||
Name: "Име",
|
||||
Status: "Статус",
|
||||
DateTime: "Датум и време",
|
||||
Message: "Порука",
|
||||
"No important events": "Нема битних догађаја",
|
||||
Resume: "Настави",
|
||||
Edit: "Измени",
|
||||
Delete: "Уклони",
|
||||
Current: "Тренутно",
|
||||
Uptime: "Време рада",
|
||||
"Cert Exp.": "Истек серт.",
|
||||
days: "дана",
|
||||
day: "дан",
|
||||
"-day": "-дана",
|
||||
hour: "сат",
|
||||
"-hour": "-сата",
|
||||
Response: "Одговор",
|
||||
Ping: "Пинг",
|
||||
"Monitor Type": "Тип посматрача",
|
||||
Keyword: "Кључна реч",
|
||||
"Friendly Name": "Пријатељско име",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Порт",
|
||||
"Heartbeat Interval": "Интервал откуцаја срца",
|
||||
Retries: "Покушаји",
|
||||
Advanced: "Напредно",
|
||||
"Upside Down Mode": "Наопак мод",
|
||||
"Max. Redirects": "Макс. преусмерења",
|
||||
"Accepted Status Codes": "Прихваћени статусни кодови",
|
||||
Save: "Сачувај",
|
||||
Notifications: "Обавештења",
|
||||
"Not available, please setup.": "Није доступно, молим Вас подесите.",
|
||||
"Setup Notification": "Постави обавештење",
|
||||
Light: "Светло",
|
||||
Dark: "Тамно",
|
||||
Auto: "Аутоматско",
|
||||
"Theme - Heartbeat Bar": "Тема - Трака откуцаја срца",
|
||||
Normal: "Нормално",
|
||||
Bottom: "Доле",
|
||||
None: "Искључено",
|
||||
Timezone: "Временска зона",
|
||||
"Search Engine Visibility": "Видљивост претраживачима",
|
||||
"Allow indexing": "Дозволи индексирање",
|
||||
"Discourage search engines from indexing site": "Одвраћајте претраживаче од индексирања сајта",
|
||||
"Change Password": "Промени лозинку",
|
||||
"Current Password": "Тренутна лозинка",
|
||||
"New Password": "Нова лозинка",
|
||||
"Repeat New Password": "Понови нову лозинку",
|
||||
"Update Password": "Измени лозинку",
|
||||
"Disable Auth": "Искључи аутентификацију",
|
||||
"Enable Auth": "Укључи аутентификацију",
|
||||
Logout: "Одлогуј се",
|
||||
Leave: "Изађи",
|
||||
"I understand, please disable": "Разумем, молим те искључи",
|
||||
Confirm: "Потврди",
|
||||
Yes: "Да",
|
||||
No: "Не",
|
||||
Username: "Корисничко име",
|
||||
Password: "Лозинка",
|
||||
"Remember me": "Запамти ме",
|
||||
Login: "Улогуј се",
|
||||
"No Monitors, please": "Без посматрача молим",
|
||||
"add one": "додај једног",
|
||||
"Notification Type": "Тип обавештења",
|
||||
Email: "Е-пошта",
|
||||
Test: "Тест",
|
||||
"Certificate Info": "Информације сертификата",
|
||||
"Resolver Server": "Разрешивачки сервер",
|
||||
"Resource Record Type": "Тип записа ресурса",
|
||||
"Last Result": "Последњи резултат",
|
||||
"Create your admin account": "Наприви администраторски налог",
|
||||
"Repeat Password": "Поновите лозинку",
|
||||
respTime: "Време одг. (мс)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/sv-SE.js
Normal file
112
src/languages/sv-SE.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "Swedish",
|
||||
checkEverySecond: "Uppdatera var {0} sekund.",
|
||||
"Avg.": "Genomsnitt ",
|
||||
retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
|
||||
ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
|
||||
upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",
|
||||
maxRedirectDescription: "Max antal omdirigeringar att följa. Välj 0 för att avaktivera omdirigeringar.",
|
||||
acceptedStatusCodesDescription: "Välj statuskoder som räknas som lyckade.",
|
||||
passwordNotMatchMsg: "Det bekräftade lösenordet stämmer ej överens.",
|
||||
notificationDescription: "Vänligen lägg till en notistjänst till övervakaren.",
|
||||
keywordDescription: "Sök efter nyckelord i ren HTML eller JSON-svar. Sökningen är skiftkänslig.",
|
||||
pauseDashboardHome: "Pausa",
|
||||
deleteMonitorMsg: "Är du säker på att du vill ta bort den här övervakningen?",
|
||||
deleteNotificationMsg: "Är du säker på att du vill ta bort den här notisen för alla övervakare?",
|
||||
resoverserverDescription: "Cloudflare är den förvalda servern. Du kan byta resolver när som helst.",
|
||||
rrtypeDescription: "Välj den RR-typ du vill övervaka",
|
||||
pauseMonitorMsg: "Är du säker på att du vill pausa?",
|
||||
Settings: "Inställningar",
|
||||
Dashboard: "Infopanel",
|
||||
"New Update": "Ny uppdatering",
|
||||
Language: "Språk",
|
||||
Appearance: "Utseende",
|
||||
Theme: "Tema",
|
||||
General: "Allmänt",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Sök efter uppdatering på GitHub",
|
||||
List: "Lista",
|
||||
Add: "Lägg till",
|
||||
"Add New Monitor": "Lägg Till Ny Övervakare",
|
||||
"Quick Stats": "Snabbstatistik",
|
||||
Up: "Uppe",
|
||||
Down: "Nere",
|
||||
Pending: "Pågående",
|
||||
Unknown: "Okänt",
|
||||
Pause: "Paus",
|
||||
Name: "Namn",
|
||||
Status: "Status",
|
||||
DateTime: "DatumTid",
|
||||
Message: "Meddelande",
|
||||
"No important events": "Inga viktiga händelser",
|
||||
Resume: "Återuppta",
|
||||
Edit: "Redigera",
|
||||
Delete: "Ta bort",
|
||||
Current: "Nuvarande",
|
||||
Uptime: "Drifttid",
|
||||
"Cert Exp.": "Certifikatsutgång",
|
||||
days: "dagar",
|
||||
day: "dag",
|
||||
"-day": "-dag",
|
||||
hour: "timme",
|
||||
"-hour": "-timme",
|
||||
Response: "Svar",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Övervakningstyp",
|
||||
Keyword: "Nyckelord",
|
||||
"Friendly Name": "Vänligt Namn",
|
||||
URL: "URL",
|
||||
Hostname: "Värdnamn",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Hjärtslagsintervall",
|
||||
Retries: "Försök",
|
||||
Advanced: "Avancerat",
|
||||
"Upside Down Mode": "Upp och ner-läge",
|
||||
"Max. Redirects": "Max antal omdirigeringar",
|
||||
"Accepted Status Codes": "Tillåtna statuskoder",
|
||||
Save: "Spara",
|
||||
Notifications: "Notiser",
|
||||
"Not available, please setup.": "Ej tillgänglig, vänligen konfigurera.",
|
||||
"Setup Notification": "Konfigurera Notis",
|
||||
Light: "Ljust",
|
||||
Dark: "Mörkt",
|
||||
Auto: "Automatisk",
|
||||
"Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
|
||||
Normal: "Normal",
|
||||
Bottom: "Botten",
|
||||
None: "Ingen",
|
||||
Timezone: "Tidszon",
|
||||
"Search Engine Visibility": "Synlighet på Sökmotorer",
|
||||
"Allow indexing": "Tillåt indexering",
|
||||
"Discourage search engines from indexing site": "Hindra sökmotorer från att indexera sidan",
|
||||
"Change Password": "Byt Lösenord",
|
||||
"Current Password": "Nuvarande Lösenord",
|
||||
"New Password": "Nytt Lösenord",
|
||||
"Repeat New Password": "Upprepa Nytt Lösenord",
|
||||
"Update Password": "Uppdatera Lösenord",
|
||||
"Disable Auth": "Avaktivera Autentisering",
|
||||
"Enable Auth": "Aktivera Autentisering",
|
||||
Logout: "Logga ut",
|
||||
Leave: "Lämna",
|
||||
"I understand, please disable": "Jag förstår, vänligen avaktivera",
|
||||
Confirm: "Bekräfta",
|
||||
Yes: "Ja",
|
||||
No: "Nej",
|
||||
Username: "Användarnamn",
|
||||
Password: "Lösenord",
|
||||
"Remember me": "Kom ihåg mig",
|
||||
Login: "Logga in",
|
||||
"No Monitors, please": "Inga Övervakare, tack",
|
||||
"add one": "lägg till en",
|
||||
"Notification Type": "Notistyp",
|
||||
Email: "Email",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Certifikatsinfo",
|
||||
"Resolver Server": "Resolverserver",
|
||||
"Resource Record Type": "RR-typ",
|
||||
"Last Result": "Senaste resultat",
|
||||
"Create your admin account": "Skapa ditt administratörskonto",
|
||||
"Repeat Password": "Upprepa Lösenord",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/zh-CN.js
Normal file
112
src/languages/zh-CN.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "简体中文",
|
||||
checkEverySecond: "检测频率 {0} 秒",
|
||||
"Avg.": "平均",
|
||||
retriesDescription: "最大重试失败次数",
|
||||
ignoreTLSError: "忽略HTTPS站点的证书错误",
|
||||
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
|
||||
maxRedirectDescription: "最大重定向次数,设置为 0 禁止重定向",
|
||||
acceptedStatusCodesDescription: "选择被视为成功响应的状态码",
|
||||
passwordNotMatchMsg: "两次密码输入不一致",
|
||||
notificationDescription: "请为监控项配置消息通知",
|
||||
keywordDescription: "检测响应内容中的关键字,区分大小写",
|
||||
pauseDashboardHome: "暂停",
|
||||
deleteMonitorMsg: "确定要删除此监控吗?",
|
||||
deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。",
|
||||
resoverserverDescription: "默认服务器 Cloudflare,可以修改为任意你想要使用的DNS服务器",
|
||||
rrtypeDescription: "选择要监控的资源记录类型",
|
||||
pauseMonitorMsg: "确定要暂停吗?",
|
||||
Settings: "设置",
|
||||
Dashboard: "仪表盘",
|
||||
"New Update": "有新版本更新",
|
||||
Language: "语言",
|
||||
Appearance: "外观设置",
|
||||
Theme: "主题",
|
||||
General: "基本设置",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "检查更新",
|
||||
List: "列表",
|
||||
Add: "添加",
|
||||
"Add New Monitor": "创建监控项",
|
||||
"Quick Stats": "状态速览",
|
||||
Up: "正常",
|
||||
Down: "故障",
|
||||
Pending: "检测失败",
|
||||
Unknown: "未知",
|
||||
Pause: "暂停",
|
||||
Name: "名称",
|
||||
Status: "状态",
|
||||
DateTime: "时间",
|
||||
Message: "事件",
|
||||
"No important events": "暂无重要事件",
|
||||
Resume: "恢复",
|
||||
Edit: "修改",
|
||||
Delete: "删除",
|
||||
Current: "当前",
|
||||
Uptime: "可用率",
|
||||
"Cert Exp.": "证书过期",
|
||||
days: "天",
|
||||
day: "天",
|
||||
"-day": " 天",
|
||||
hour: "小时",
|
||||
"-hour": " 小时",
|
||||
Response: "响应时长",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "监控类型",
|
||||
Keyword: "关键字",
|
||||
"Friendly Name": "自定义名称",
|
||||
URL: "网址URL",
|
||||
Hostname: "主机名",
|
||||
Port: "端口号",
|
||||
"Heartbeat Interval": "心跳间隔",
|
||||
Retries: "重试次数",
|
||||
Advanced: "高级选项",
|
||||
"Upside Down Mode": "反向监控",
|
||||
"Max. Redirects": "重定向次数",
|
||||
"Accepted Status Codes": "有效状态码",
|
||||
Save: "保存",
|
||||
Notifications: "消息通知",
|
||||
"Not available, please setup.": "无可用通道,请先设置",
|
||||
"Setup Notification": "设置通知",
|
||||
Light: "明亮",
|
||||
Dark: "黑暗",
|
||||
Auto: "自动",
|
||||
"Theme - Heartbeat Bar": "状态显示",
|
||||
Normal: "正常显示",
|
||||
Bottom: "靠下显示",
|
||||
None: "不显示",
|
||||
Timezone: "时区",
|
||||
"Search Engine Visibility": "搜索引擎设置",
|
||||
"Allow indexing": "允许索引",
|
||||
"Discourage search engines from indexing site": "阻止搜索引擎索引网站",
|
||||
"Change Password": "修改密码",
|
||||
"Current Password": "当前密码",
|
||||
"New Password": "新的密码",
|
||||
"Repeat New Password": "重复新的密码",
|
||||
"Update Password": "更新密码",
|
||||
"Disable Auth": "禁用身份验证",
|
||||
"Enable Auth": "启用身份验证",
|
||||
Logout: "退出",
|
||||
Leave: "离开",
|
||||
"I understand, please disable": "我已了解,继续禁用",
|
||||
Confirm: "确认",
|
||||
Yes: "确定",
|
||||
No: "取消",
|
||||
Username: "用户名",
|
||||
Password: "密码",
|
||||
"Remember me": "记住登录",
|
||||
Login: "登录",
|
||||
"No Monitors, please": "还没有监控项,",
|
||||
"add one": "点击新增",
|
||||
"Notification Type": "消息类型",
|
||||
Email: "邮件",
|
||||
Test: "测试一下",
|
||||
"Certificate Info": "证书信息",
|
||||
"Resolver Server": "解析服务器",
|
||||
"Resource Record Type": "资源记录类型",
|
||||
"Last Result": "Last Result",
|
||||
"Create your admin account": "创建管理员账号",
|
||||
"Repeat Password": "重复密码",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
112
src/languages/zh-HK.js
Normal file
112
src/languages/zh-HK.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export default {
|
||||
languageName: "繁體中文 (香港)",
|
||||
Settings: "設定",
|
||||
Dashboard: "錶板",
|
||||
"New Update": "有更新",
|
||||
Language: "語言",
|
||||
Appearance: "外觀",
|
||||
Theme: "主題",
|
||||
General: "一般",
|
||||
Version: "版本",
|
||||
"Check Update On GitHub": "到 Github 查看更新",
|
||||
List: "列表",
|
||||
Add: "新增",
|
||||
"Add New Monitor": "新增監測器",
|
||||
"Quick Stats": "綜合數據",
|
||||
Up: "上線",
|
||||
Down: "離線",
|
||||
Pending: "待定",
|
||||
Unknown: "不明",
|
||||
Pause: "暫停",
|
||||
pauseDashboardHome: "暫停",
|
||||
Name: "名稱",
|
||||
Status: "狀態",
|
||||
DateTime: "日期時間",
|
||||
Message: "內容",
|
||||
"No important events": "沒有重要事件",
|
||||
Resume: "恢復",
|
||||
Edit: "編輯",
|
||||
Delete: "刪除",
|
||||
Current: "目前",
|
||||
Uptime: "上線率",
|
||||
"Cert Exp.": "証書期限",
|
||||
days: "日",
|
||||
day: "日",
|
||||
"-day": "日",
|
||||
hour: "小時",
|
||||
"-hour": "小時",
|
||||
checkEverySecond: "每 {0} 秒檢查一次",
|
||||
"Avg.": "平均",
|
||||
Response: "反應時間",
|
||||
Ping: "反應時間",
|
||||
"Monitor Type": "監測器類型",
|
||||
Keyword: "關鍵字",
|
||||
"Friendly Name": "名稱",
|
||||
URL: "網址 URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "檢查間距",
|
||||
Retries: "重試數次確定為離線",
|
||||
retriesDescription: "重試多少次後才判定為離線及傳送通知。如數值為 0 會即判定為離線及傳送通知。",
|
||||
Advanced: "進階",
|
||||
ignoreTLSError: "忽略 TLS/SSL 錯誤",
|
||||
"Upside Down Mode": "反轉模式",
|
||||
upsideDownModeDescription: "反轉狀態,如網址是可正常瀏覽,會被判定為 '離線/DOWN'",
|
||||
"Max. Redirects": "跟隨重新導向 (Redirect) 的次數",
|
||||
maxRedirectDescription: "設為 0 即不跟蹤",
|
||||
"Accepted Status Codes": "接受為上線的 HTTP 狀態碼",
|
||||
acceptedStatusCodesDescription: "可多選",
|
||||
Save: "儲存",
|
||||
Notifications: "通知",
|
||||
"Not available, please setup.": "無法使用,需要設定",
|
||||
"Setup Notification": "設定通知",
|
||||
Light: "明亮",
|
||||
Dark: "暗黑",
|
||||
Auto: "自動",
|
||||
"Theme - Heartbeat Bar": "監測器列表 狀態條外觀",
|
||||
Normal: "一般",
|
||||
Bottom: "下方",
|
||||
None: "沒有",
|
||||
Timezone: "時區",
|
||||
"Search Engine Visibility": "是否允許搜尋器索引",
|
||||
"Allow indexing": "允許索引",
|
||||
"Discourage search engines from indexing site": "不建議搜尋器索引",
|
||||
"Change Password": "變更密碼",
|
||||
"Current Password": "目前密碼",
|
||||
"New Password": "新密碼",
|
||||
"Repeat New Password": "確認新密碼",
|
||||
passwordNotMatchMsg: "密碼不一致",
|
||||
"Update Password": "更新密碼",
|
||||
"Disable Auth": "取消登入認証",
|
||||
"Enable Auth": "開啟登入認証",
|
||||
Logout: "登出",
|
||||
notificationDescription: "新增後,你需要在監測器裡啟用。",
|
||||
Leave: "離開",
|
||||
"I understand, please disable": "我明白,請取消登入認証",
|
||||
Confirm: "確認",
|
||||
Yes: "是",
|
||||
No: "否",
|
||||
Username: "帳號",
|
||||
Password: "密碼",
|
||||
"Remember me": "記住我",
|
||||
Login: "登入",
|
||||
"No Monitors, please": "沒有監測器,請",
|
||||
"add one": "新增",
|
||||
"Notification Type": "通知類型",
|
||||
Email: "電郵",
|
||||
Test: "測試",
|
||||
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
||||
"Certificate Info": "憑證詳細資料",
|
||||
deleteMonitorMsg: "是否確定刪除這個監測器",
|
||||
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
||||
"Resolver Server": "DNS 伺服器",
|
||||
"Resource Record Type": "DNS 記錄類型",
|
||||
resoverserverDescription: "預設值為 Cloudflare DNS 伺服器,你可以轉用其他 DNS 伺服器。",
|
||||
rrtypeDescription: "請選擇 DNS 記錄類型",
|
||||
pauseMonitorMsg: "是否確定暫停?",
|
||||
"Last Result": "最後結果",
|
||||
"Create your admin account": "製作你的管理員帳號",
|
||||
"Repeat Password": "重複密碼",
|
||||
respTime: "反應時間 (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
@@ -3,11 +3,5 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -1,91 +1,131 @@
|
||||
<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 :class="classes">
|
||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||
<div class="container-fluid">
|
||||
{{ $root.connectionErrorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
|
||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
|
||||
</a>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<!-- Mobile header -->
|
||||
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<router-view v-if="$root.loggedIn" />
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
</main>
|
||||
|
||||
<!-- Mobile Only -->
|
||||
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
|
||||
<nav v-if="$root.isMobile" class="bottom-nav">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||
{{ $t("Dashboard") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/list" class="nav-link">
|
||||
<div><font-awesome-icon icon="list" /></div>
|
||||
{{ $t("List") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/add" class="nav-link">
|
||||
<div><font-awesome-icon icon="plus" /></div>
|
||||
{{ $t("Add") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<div><font-awesome-icon icon="cog" /></div>
|
||||
{{ $t("Settings") }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</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"></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">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";
|
||||
import compareVersions from "compare-versions";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Login
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
components: {
|
||||
Login,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
// Theme or Mobile
|
||||
classes() {
|
||||
const classes = {};
|
||||
classes[this.$root.theme] = true;
|
||||
classes["mobile"] = this.$root.isMobile;
|
||||
return classes;
|
||||
},
|
||||
|
||||
hasNewVersion() {
|
||||
if (this.$root.info.latestVersion && this.$root.info.version) {
|
||||
return compareVersions(this.$root.info.latestVersion, this.$root.info.version) >= 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
|
||||
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">
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.bottom-nav {
|
||||
@@ -99,7 +139,7 @@ export default {
|
||||
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;
|
||||
padding: 0 10px;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
@@ -123,6 +163,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -137,15 +181,19 @@ export default {
|
||||
color: white;
|
||||
}
|
||||
|
||||
main {
|
||||
.dark {
|
||||
header {
|
||||
background-color: #161b22;
|
||||
border-bottom-color: #161b22 !important;
|
||||
|
||||
}
|
||||
span {
|
||||
color: #f0f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #AAA;
|
||||
font-size: 13px;
|
||||
margin-bottom: 30px;
|
||||
margin-left: 10px;
|
||||
.bottom-nav {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
117
src/main.js
117
src/main.js
@@ -1,58 +1,84 @@
|
||||
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 "bootstrap";
|
||||
import { createApp, h } from "vue";
|
||||
import { createI18n } from "vue-i18n"
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
import App from "./App.vue";
|
||||
import "./assets/app.scss";
|
||||
import { FontAwesomeIcon } from "./icon.js";
|
||||
import EmptyLayout from "./layouts/EmptyLayout.vue";
|
||||
import Layout from "./layouts/Layout.vue";
|
||||
import socket from "./mixins/socket";
|
||||
import theme from "./mixins/theme";
|
||||
import mobile from "./mixins/mobile";
|
||||
import datetime from "./mixins/datetime";
|
||||
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 Settings from "./pages/Settings.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
import List from "./pages/List.vue";
|
||||
|
||||
import { appName } from "./util.ts";
|
||||
|
||||
import en from "./languages/en";
|
||||
import zhHK from "./languages/zh-HK";
|
||||
import deDE from "./languages/de-DE";
|
||||
import nlNL from "./languages/nl-NL";
|
||||
import esEs from "./languages/es-ES";
|
||||
import fr from "./languages/fr";
|
||||
import ja from "./languages/ja";
|
||||
import daDK from "./languages/da-DK";
|
||||
import sr from "./languages/sr";
|
||||
import srLatn from "./languages/sr-latn";
|
||||
import svSE from "./languages/sv-SE";
|
||||
import koKR from "./languages/ko-KR";
|
||||
import ruRU from "./languages/ru-RU";
|
||||
import zhCN from "./languages/zh-CN";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
path: "/",
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
name: "root",
|
||||
path: '',
|
||||
path: "",
|
||||
component: Dashboard,
|
||||
children: [
|
||||
{
|
||||
name: "DashboardHome",
|
||||
path: '/dashboard',
|
||||
path: "/dashboard",
|
||||
component: DashboardHome,
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard/:id',
|
||||
path: "/dashboard/:id",
|
||||
component: EmptyLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: "",
|
||||
component: Details,
|
||||
},
|
||||
{
|
||||
path: '/edit/:id',
|
||||
path: "/edit/:id",
|
||||
component: EditMonitor,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/add',
|
||||
path: "/add",
|
||||
component: EditMonitor,
|
||||
},
|
||||
]
|
||||
{
|
||||
path: "/list",
|
||||
component: List,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
},
|
||||
],
|
||||
@@ -62,31 +88,66 @@ const routes = [
|
||||
|
||||
},
|
||||
{
|
||||
path: '/setup',
|
||||
path: "/setup",
|
||||
component: Setup,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
linkActiveClass: 'active',
|
||||
linkActiveClass: "active",
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
const languageList = {
|
||||
en,
|
||||
"zh-HK": zhHK,
|
||||
"de-DE": deDE,
|
||||
"nl-NL": nlNL,
|
||||
"es-ES": esEs,
|
||||
"fr": fr,
|
||||
"ja": ja,
|
||||
"da-DK": daDK,
|
||||
"sr": sr,
|
||||
"sr-latn": srLatn,
|
||||
"sv-SE": svSE,
|
||||
"ko-KR": koKR,
|
||||
"ru-RU": ruRU,
|
||||
"zh-CN": zhCN,
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: localStorage.locale || "en",
|
||||
fallbackLocale: "en",
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
messages: languageList
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
mixins: [
|
||||
socket,
|
||||
theme,
|
||||
mobile,
|
||||
datetime
|
||||
],
|
||||
render: ()=>h(App)
|
||||
data() {
|
||||
return {
|
||||
appName: appName
|
||||
}
|
||||
},
|
||||
render: () => h(App),
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
|
||||
const options = {
|
||||
position: "bottom-right"
|
||||
position: "bottom-right",
|
||||
};
|
||||
|
||||
app.use(Toast, options);
|
||||
|
||||
app.mount('#app')
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon)
|
||||
|
||||
app.mount("#app")
|
||||
|
57
src/mixins/datetime.js
Normal file
57
src/mixins/datetime.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
/**
|
||||
* DateTime Mixin
|
||||
* Handled timezone and format
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userTimezone: localStorage.timezone || "auto",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
datetime(value) {
|
||||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
date(value) {
|
||||
return this.datetimeFormat(value, "YYYY-MM-DD");
|
||||
},
|
||||
|
||||
time(value, second = true) {
|
||||
let secondString;
|
||||
if (second) {
|
||||
secondString = ":ss";
|
||||
} else {
|
||||
secondString = "";
|
||||
}
|
||||
return this.datetimeFormat(value, "HH:mm" + secondString);
|
||||
},
|
||||
|
||||
datetimeFormat(value, format) {
|
||||
if (value !== undefined && value !== "") {
|
||||
return dayjs.utc(value).tz(this.timezone).format(format);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timezone() {
|
||||
if (this.userTimezone === "auto") {
|
||||
return dayjs.tz.guess()
|
||||
}
|
||||
|
||||
return this.userTimezone
|
||||
},
|
||||
}
|
||||
|
||||
}
|
25
src/mixins/mobile.js
Normal file
25
src/mixins/mobile.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
windowWidth: window.innerWidth,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.windowWidth <= 767.98;
|
||||
},
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
import {io} from "socket.io-client";
|
||||
import { useToast } from 'vue-toastification'
|
||||
import dayjs from "dayjs";
|
||||
import { io } from "socket.io-client";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast()
|
||||
|
||||
let socket;
|
||||
@@ -17,7 +16,6 @@ export default {
|
||||
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: { },
|
||||
@@ -25,43 +23,60 @@ export default {
|
||||
importantHeartbeatList: { },
|
||||
avgPingList: { },
|
||||
uptimeList: { },
|
||||
certInfoList: {},
|
||||
notificationList: [],
|
||||
windowWidth: window.innerWidth,
|
||||
showListMobile: false,
|
||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
let wsHost;
|
||||
if (localStorage.dev === "dev") {
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
wsHost = ":3001"
|
||||
} else {
|
||||
wsHost = ""
|
||||
}
|
||||
|
||||
socket = io(wsHost, {
|
||||
transports: ['websocket']
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on('info', (info) => {
|
||||
socket.on("info", (info) => {
|
||||
this.info = info;
|
||||
});
|
||||
|
||||
socket.on('setup', (monitorID, data) => {
|
||||
socket.on("setup", (monitorID, data) => {
|
||||
this.$router.push("/setup")
|
||||
});
|
||||
|
||||
socket.on('monitorList', (data) => {
|
||||
socket.on("autoLogin", (monitorID, data) => {
|
||||
this.loggedIn = true;
|
||||
this.storage().token = "autoLogin";
|
||||
this.allowLoginDialog = false;
|
||||
});
|
||||
|
||||
socket.on("monitorList", (data) => {
|
||||
// Add Helper function
|
||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||
monitor.getUrl = () => {
|
||||
try {
|
||||
return new URL(monitor.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
this.monitorList = data;
|
||||
});
|
||||
|
||||
socket.on('notificationList', (data) => {
|
||||
socket.on("notificationList", (data) => {
|
||||
this.notificationList = data;
|
||||
});
|
||||
|
||||
socket.on('heartbeat', (data) => {
|
||||
socket.on("heartbeat", (data) => {
|
||||
if (! (data.monitorID in this.heartbeatList)) {
|
||||
this.heartbeatList[data.monitorID] = [];
|
||||
}
|
||||
@@ -84,7 +99,6 @@ export default {
|
||||
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
||||
}
|
||||
|
||||
|
||||
if (! (data.monitorID in this.importantHeartbeatList)) {
|
||||
this.importantHeartbeatList[data.monitorID] = [];
|
||||
}
|
||||
@@ -93,7 +107,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('heartbeatList', (monitorID, data) => {
|
||||
socket.on("heartbeatList", (monitorID, data) => {
|
||||
if (! (monitorID in this.heartbeatList)) {
|
||||
this.heartbeatList[monitorID] = data;
|
||||
} else {
|
||||
@@ -101,15 +115,19 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('avgPing', (monitorID, data) => {
|
||||
socket.on("avgPing", (monitorID, data) => {
|
||||
this.avgPingList[monitorID] = data
|
||||
});
|
||||
|
||||
socket.on('uptime', (monitorID, type, data) => {
|
||||
socket.on("uptime", (monitorID, type, data) => {
|
||||
this.uptimeList[`${monitorID}_${type}`] = data
|
||||
});
|
||||
|
||||
socket.on('importantHeartbeatList', (monitorID, 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 {
|
||||
@@ -117,12 +135,20 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||
this.socket.connected = false;
|
||||
this.socket.firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("disconnect")
|
||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||
this.socket.connected = false;
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.on("connect", () => {
|
||||
console.log("connect")
|
||||
this.socket.connectCount++;
|
||||
this.socket.connected = true;
|
||||
@@ -132,8 +158,22 @@ export default {
|
||||
this.clearData()
|
||||
}
|
||||
|
||||
if (this.storage().token) {
|
||||
this.loginByToken(this.storage().token)
|
||||
let token = this.storage().token;
|
||||
|
||||
if (token) {
|
||||
if (token !== "autoLogin") {
|
||||
this.loginByToken(token)
|
||||
} else {
|
||||
|
||||
// Timeout if it is not actually auto login
|
||||
setTimeout(() => {
|
||||
if (! this.loggedIn) {
|
||||
this.allowLoginDialog = true;
|
||||
this.$root.storage().removeItem("token");
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
} else {
|
||||
this.allowLoginDialog = true;
|
||||
}
|
||||
@@ -145,20 +185,12 @@ export default {
|
||||
|
||||
methods: {
|
||||
|
||||
cancelActiveList() {
|
||||
this.$root.showListMobile = false;
|
||||
},
|
||||
|
||||
onResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
|
||||
storage() {
|
||||
return (this.remember) ? localStorage : sessionStorage;
|
||||
},
|
||||
|
||||
getSocket() {
|
||||
return socket;
|
||||
return socket;
|
||||
},
|
||||
|
||||
toastRes(res) {
|
||||
@@ -181,7 +213,7 @@ export default {
|
||||
this.loggedIn = true;
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, '')
|
||||
history.pushState({}, "")
|
||||
}
|
||||
|
||||
callback(res)
|
||||
@@ -226,20 +258,6 @@ export default {
|
||||
|
||||
computed: {
|
||||
|
||||
isMobile() {
|
||||
return this.windowWidth <= 767.98;
|
||||
},
|
||||
|
||||
timezone() {
|
||||
|
||||
if (this.userTimezone === "auto") {
|
||||
return dayjs.tz.guess()
|
||||
} else {
|
||||
return this.userTimezone
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
lastHeartbeatList() {
|
||||
let result = {}
|
||||
|
||||
@@ -256,7 +274,7 @@ export default {
|
||||
|
||||
let unknown = {
|
||||
text: "Unknown",
|
||||
color: "secondary"
|
||||
color: "secondary",
|
||||
}
|
||||
|
||||
for (let monitorID in this.lastHeartbeatList) {
|
||||
@@ -267,12 +285,17 @@ export default {
|
||||
} else if (lastHeartBeat.status === 1) {
|
||||
result[monitorID] = {
|
||||
text: "Up",
|
||||
color: "primary"
|
||||
color: "primary",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 0) {
|
||||
result[monitorID] = {
|
||||
text: "Down",
|
||||
color: "danger"
|
||||
color: "danger",
|
||||
};
|
||||
} else if (lastHeartBeat.status === 2) {
|
||||
result[monitorID] = {
|
||||
text: "Pending",
|
||||
color: "warning",
|
||||
};
|
||||
} else {
|
||||
result[monitorID] = unknown;
|
||||
@@ -280,23 +303,22 @@ export default {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
// Reload the SPA if the server version is changed.
|
||||
"info.version"(to, from) {
|
||||
if (from && from !== to) {
|
||||
window.location.reload()
|
||||
}
|
||||
if (from && from !== to) {
|
||||
window.location.reload()
|
||||
}
|
||||
},
|
||||
|
||||
remember() {
|
||||
localStorage.remember = (this.remember) ? "1" : "0"
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
66
src/mixins/theme.js
Normal file
66
src/mixins/theme.js
Normal file
@@ -0,0 +1,66 @@
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||
userTheme: localStorage.theme,
|
||||
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Default Light
|
||||
if (! this.userTheme) {
|
||||
this.userTheme = "light";
|
||||
}
|
||||
|
||||
// Default Heartbeat Bar
|
||||
if (!this.userHeartbeatBar) {
|
||||
this.userHeartbeatBar = "normal";
|
||||
}
|
||||
|
||||
document.body.classList.add(this.theme);
|
||||
this.updateThemeColorMeta();
|
||||
},
|
||||
|
||||
computed: {
|
||||
theme() {
|
||||
if (this.userTheme === "auto") {
|
||||
return this.system;
|
||||
}
|
||||
return this.userTheme;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
userTheme(to, from) {
|
||||
localStorage.theme = to;
|
||||
},
|
||||
|
||||
theme(to, from) {
|
||||
document.body.classList.remove(from);
|
||||
document.body.classList.add(this.theme);
|
||||
this.updateThemeColorMeta();
|
||||
},
|
||||
|
||||
userHeartbeatBar(to, from) {
|
||||
localStorage.heartbeatBarTheme = to;
|
||||
},
|
||||
|
||||
heartbeatBarTheme(to, from) {
|
||||
document.body.classList.remove(from);
|
||||
document.body.classList.add(this.heartbeatBarTheme);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateThemeColorMeta() {
|
||||
if (this.theme === "dark") {
|
||||
document.querySelector("#theme-color").setAttribute("content", "#161B22");
|
||||
} else {
|
||||
document.querySelector("#theme-color").setAttribute("content", "#5cdd8b");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user