mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 15:36:59 +08:00
Compare commits
463 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cf10e26aff | ||
|
c0fe2d54f9 | ||
|
d8303f1f4d | ||
|
ee14ab6751 | ||
|
87e45b21fa | ||
|
cbc0b9c553 | ||
|
c2472bf750 | ||
|
84fad93555 | ||
|
2975204a0a | ||
|
31150642cd | ||
|
3c5c49c16d | ||
|
5c25354682 | ||
|
2aad2510b7 | ||
|
fac2f1cbc6 | ||
|
8bc3651a7d | ||
|
684d0a7eb8 | ||
|
b3712ee1cc | ||
|
af94424283 | ||
|
728e811969 | ||
|
a6007adce3 | ||
|
30b72d81cf | ||
|
de6e1e7ddd | ||
|
2af754b5e8 | ||
|
3b3763351b | ||
|
9a488d6968 | ||
|
aca395cea1 | ||
|
a49faf09b9 | ||
|
d0d1e0de28 | ||
|
70aa8fe453 | ||
|
19d8761305 | ||
|
d6a113396a | ||
|
fb3fe17c28 | ||
|
71d62ee151 | ||
|
82b9bfc5a0 | ||
|
f016caa513 | ||
|
6e29feffd3 | ||
|
2389b604fe | ||
|
a3b1612938 | ||
|
a07f54f35b | ||
|
b777c0c3e4 | ||
|
bea8679788 | ||
|
f091e92c70 | ||
|
a0843745f9 | ||
|
16d6885a88 | ||
|
4d975a5bd5 | ||
|
96ec46765b | ||
|
96971f6776 | ||
|
ffb1a948fe | ||
|
4e4156285a | ||
|
1223b56205 | ||
|
8ced61697a | ||
|
f3322398e5 | ||
|
b76ca59dfe | ||
|
554b0d2bc3 | ||
|
4575f31094 | ||
|
df7f0b078d | ||
|
d8253405b4 | ||
|
56c6c0c6f1 | ||
|
b16cb6a337 | ||
|
694b4cadb3 | ||
|
75f6ff8b58 | ||
|
1062e629c5 | ||
|
13f7db655b | ||
|
60e7824ff0 | ||
|
fb3b407577 | ||
|
d54c652d26 | ||
|
f1bcecb0c6 | ||
|
88afd662db | ||
|
e356d5f623 | ||
|
0d098b0958 | ||
|
239611a016 | ||
|
2ccf1fe41b | ||
|
77340cf0d2 | ||
|
c412c66aeb | ||
|
9a8b484ee8 | ||
|
17ed051401 | ||
|
8f7b7e74c9 | ||
|
9cd060c6c3 | ||
|
1999541802 | ||
|
65d71e5db0 | ||
|
2073f0c284 | ||
|
25d711e683 | ||
|
77a7801992 | ||
|
525607f49e | ||
|
8613d3ffa9 | ||
|
d44d984a46 | ||
|
d362372b05 | ||
|
3fa5dfc873 | ||
|
6ce012c9a1 | ||
|
f33b6de157 | ||
|
e0a2ed2523 | ||
|
a78cb7ab42 | ||
|
278d9f5689 | ||
|
2ad79a68b9 | ||
|
d29955f3ba | ||
|
c4125a8334 | ||
|
0a368ff553 | ||
|
27dbc021b4 | ||
|
1b120f8a6f | ||
|
6f57c4195a | ||
|
baa592bce3 | ||
|
624678826d | ||
|
e8c3807594 | ||
|
4d8e755400 | ||
|
9b92a02968 | ||
|
6418f99f1a | ||
|
93ac4e1b96 | ||
|
f65bef686c | ||
|
2f26864892 | ||
|
a802f7ebed | ||
|
e5e8db6c38 | ||
|
303738b7c2 | ||
|
dddd2c0042 | ||
|
515095ecfb | ||
|
83284b6d2c | ||
|
1af6d33fcd | ||
|
e36b65c2df | ||
|
8542e6cbb9 | ||
|
5dd197374d | ||
|
f84ae82983 | ||
|
9650418ef7 | ||
|
b546c846ae | ||
|
b5cbc6f5f6 | ||
|
f8def5aa6f | ||
|
6f01a448ad | ||
|
5dd3d32d77 | ||
|
ff8bba6863 | ||
|
ed1f88a852 | ||
|
0ecaa2cbd7 | ||
|
3c3dc05621 | ||
|
1f5466a3e8 | ||
|
d5da5af174 | ||
|
c36d9a4b8b | ||
|
a7063b8aca | ||
|
0a8046c98e | ||
|
7ba717ee55 | ||
|
ea400ac35f | ||
|
15db2c060d | ||
|
89717495dc | ||
|
7b710af12c | ||
|
5b278ca500 | ||
|
f1d24782f8 | ||
|
b97019eea8 | ||
|
d65abe5b8c | ||
|
c4e2d67d17 | ||
|
bcd616a4d0 | ||
|
7533041696 | ||
|
7b0deb5e20 | ||
|
b4a4171178 | ||
|
012be23509 | ||
|
dd09351c8e | ||
|
ffad990ca4 | ||
|
47e82ed83a | ||
|
e1f766756f | ||
|
4b2a465c94 | ||
|
edcdedcaae | ||
|
f7afe121e3 | ||
|
945288f0c0 | ||
|
ac27e6e2af | ||
|
869a040011 | ||
|
42848bcd2e | ||
|
a3b94aa532 | ||
|
fdbdf83a0d | ||
|
81d5360520 | ||
|
8f1e193de3 | ||
|
ac449ec1c2 | ||
|
da91317760 | ||
|
bef0febede | ||
|
7d63b700e1 | ||
|
0223f86a2a | ||
|
c170b1edd0 | ||
|
5943514a92 | ||
|
62acd2edb1 | ||
|
483cbfb636 | ||
|
660005b143 | ||
|
98f3c126e5 | ||
|
6995a29980 | ||
|
cf2ca71dee | ||
|
0bd1c42080 | ||
|
9b21b86e70 | ||
|
f723930d11 | ||
|
e425e408a2 | ||
|
c690d1c3a1 | ||
|
8abbc9fd15 | ||
|
af7c905b44 | ||
|
0e8f6d2f85 | ||
|
d16be6fb7d | ||
|
f25ca96308 | ||
|
6682839ec8 | ||
|
817f6db4fd | ||
|
dc2302244f | ||
|
7a27d3752a | ||
|
f0e8f34aeb | ||
|
54b9698a05 | ||
|
69273a6c41 | ||
|
6424fe77ab | ||
|
fa60672cce | ||
|
6e43ef1dd3 | ||
|
f4f2b8ddb8 | ||
|
436bc13aeb | ||
|
b72a279361 | ||
|
a28ef56553 | ||
|
7f432bd916 | ||
|
f570d41142 | ||
|
1c4ddaeddf | ||
|
d4485fe62f | ||
|
e1681ce370 | ||
|
69d6633e6d | ||
|
55a6e5af42 | ||
|
252709ff49 | ||
|
774fe58ddc | ||
|
5f347b10ba | ||
|
f442507cab | ||
|
a23ab9d1de | ||
|
404923b7c8 | ||
|
a41023ca2a | ||
|
817c941489 | ||
|
5f6347d277 | ||
|
fbfa5a33ed | ||
|
04e22f17a9 | ||
|
aa398948da | ||
|
11243a6ca1 | ||
|
54548e34ed | ||
|
87428231ad | ||
|
a68d945cdc | ||
|
2c0180f323 | ||
|
4fdaa1abb6 | ||
|
6ee7b3696a | ||
|
cc258dce14 | ||
|
fb420fa1b1 | ||
|
a707b51053 | ||
|
a927f5cd15 | ||
|
0e28707307 | ||
|
c94dcf1533 | ||
|
b0476cfb5b | ||
|
2170229031 | ||
|
213aca4fc3 | ||
|
2b42c3c828 | ||
|
d939d03690 | ||
|
07888e43f1 | ||
|
c6c1bb5b5c | ||
|
3210264e28 | ||
|
54e948c2ca | ||
|
80094ec4e1 | ||
|
091158cfe7 | ||
|
abb6ce2366 | ||
|
e4ad8cbfc8 | ||
|
a674caa520 | ||
|
179e3569b5 | ||
|
43527f2f40 | ||
|
26ff6f45a0 | ||
|
c095767f4a | ||
|
ffb7ba176c | ||
|
857e88b27e | ||
|
90fe25e8ad | ||
|
46a593534b | ||
|
7a4b54f4ee | ||
|
ea10d89f51 | ||
|
7f46223d68 | ||
|
df4ce811d9 | ||
|
30858ab038 | ||
|
e25d406fa5 | ||
|
10e16782b1 | ||
|
107a44885c | ||
|
ef9f66fad9 | ||
|
e9e78c26e5 | ||
|
46dae99695 | ||
|
edd9bf3887 | ||
|
ab4edf2092 | ||
|
334cb57fed | ||
|
cfa5b551a5 | ||
|
46ee149b70 | ||
|
0a8c922abf | ||
|
058e5442af | ||
|
ea1725737f | ||
|
5566b038c8 | ||
|
5830f1e0b5 | ||
|
35b8e89457 | ||
|
d892b2c549 | ||
|
f23baf9c22 | ||
|
54184350a4 | ||
|
14dbe7c334 | ||
|
122e6a842b | ||
|
77ef22bdb4 | ||
|
59f983d506 | ||
|
71f031c14e | ||
|
6ae2a48584 | ||
|
7373747906 | ||
|
9d87f8d390 | ||
|
73b965c867 | ||
|
751e5ac477 | ||
|
93e5023ead | ||
|
32cfd411f8 | ||
|
a9f3142cee | ||
|
b7ba6330db | ||
|
4c3aa20eb0 | ||
|
f779c6286a | ||
|
da99a57560 | ||
|
42d68edab0 | ||
|
019d638767 | ||
|
9fc5a3329f | ||
|
23c4ece2a5 | ||
|
175556f9fc | ||
|
398219f847 | ||
|
7a50f0e3f3 | ||
|
4178b003a2 | ||
|
8ede6d888f | ||
|
cec0521834 | ||
|
73b603dd10 | ||
|
92a43e1f30 | ||
|
0cf395dfc3 | ||
|
749ca6f4a8 | ||
|
ef73af391f | ||
|
44f6fca945 | ||
|
23ce7c6623 | ||
|
c346ea7864 | ||
|
f0ad32a252 | ||
|
5720017fb4 | ||
|
b7dc8e3ef8 | ||
|
5bba19f866 | ||
|
e198f2f1ab | ||
|
f91e5b98f9 | ||
|
87f933df4f | ||
|
332b9ab248 | ||
|
7cc89979f0 | ||
|
398ecb7666 | ||
|
668e97c5a9 | ||
|
90473e7924 | ||
|
fdd781b081 | ||
|
373bd9b962 | ||
|
66971deaf4 | ||
|
59be9bb971 | ||
|
8077744c60 | ||
|
c5faf709b8 | ||
|
7da9f139c1 | ||
|
7acbfd2474 | ||
|
9f493bbec7 | ||
|
5bf58cc6c4 | ||
|
d344914ca0 | ||
|
201a25c659 | ||
|
b680371746 | ||
|
e488e2dc0a | ||
|
4e3258579d | ||
|
aa8ea6d398 | ||
|
cd3fbc80b4 | ||
|
bb7d67f717 | ||
|
8b0813ceff | ||
|
91178ce6a5 | ||
|
429ad384d0 | ||
|
24e52726b2 | ||
|
e0a0a5db4c | ||
|
93050208bb | ||
|
98ee9caf2c | ||
|
8e99cbf426 | ||
|
cbfecab850 | ||
|
25cc54bf72 | ||
|
3700b16c5b | ||
|
4b9dc2890d | ||
|
f9004bcbed | ||
|
bc174c3325 | ||
|
4c2753af46 | ||
|
c6ba5b621c | ||
|
96536ae391 | ||
|
ba46544772 | ||
|
5c852db1cf | ||
|
069d3765f0 | ||
|
15820c6937 | ||
|
000cbeb0ce | ||
|
e118d59ac8 | ||
|
39aa0a7f07 | ||
|
a12dffd1bc | ||
|
410805052e | ||
|
02a8147f22 | ||
|
d962ab7a1c | ||
|
63c8d24d6f | ||
|
254a6bfd36 | ||
|
29f3cbe8c6 | ||
|
53b98ad3e4 | ||
|
dbd7c087e0 | ||
|
d0546afe71 | ||
|
272956025c | ||
|
db50ba91cc | ||
|
42ea3fb412 | ||
|
9f8b3151d8 | ||
|
73e38a13d2 | ||
|
f4515ad8c5 | ||
|
369477b4b9 | ||
|
2347a01f7c | ||
|
c114c053d6 | ||
|
ae2c49a729 | ||
|
b9e72b9645 | ||
|
5a069b278d | ||
|
65ea2e6aeb | ||
|
e82fc1df61 | ||
|
7dd5f5ea0d | ||
|
45da7c5431 | ||
|
3c01e8732c | ||
|
39df4eea92 | ||
|
d1a3cd047a | ||
|
64a33d7455 | ||
|
7ed8ae9f7c | ||
|
c7ec9a07e2 | ||
|
052fde5a24 | ||
|
d6b591a513 | ||
|
19933bbd99 | ||
|
60f8ab7285 | ||
|
b7e2489d22 | ||
|
e103ac8335 | ||
|
a680331dd7 | ||
|
288ed1e3ca | ||
|
8c8eeaf627 | ||
|
b893d50e45 | ||
|
361e44ad6a | ||
|
0765f05090 | ||
|
2638d68c97 | ||
|
e38742a2d0 | ||
|
1b1e0f6dd9 | ||
|
0961c6d9b3 | ||
|
ce7d8c38c5 | ||
|
af44b0beab | ||
|
84a0b24448 | ||
|
a4be651118 | ||
|
d8013f31e8 | ||
|
91366ff565 | ||
|
454c1687cf | ||
|
244a7b3671 | ||
|
28be32fc68 | ||
|
ee90d2713f | ||
|
d446a57d42 | ||
|
855b12f435 | ||
|
f390a8caf1 | ||
|
30ce53f57c | ||
|
8c4ab9d652 | ||
|
f931e709e6 | ||
|
11e9eee09d | ||
|
65fc71e485 | ||
|
b69a8b8493 | ||
|
1ac904d6d6 | ||
|
dd3992063e | ||
|
29df70949d | ||
|
0313acd4c5 | ||
|
cd19b9fc49 | ||
|
c57b2c4d28 | ||
|
3dda5938f2 | ||
|
4818bb67d6 | ||
|
9619d31a05 | ||
|
c5cc42272f | ||
|
b0259b5592 | ||
|
f00ec4dfef | ||
|
43f8fc701c | ||
|
499042504f | ||
|
faf6719e7c | ||
|
a9d264ccfc | ||
|
df8f93f0c2 | ||
|
28c0e16a0c | ||
|
6acc9546a0 | ||
|
f455e3a454 | ||
|
7abbf421d0 | ||
|
3625915a85 | ||
|
d74404e106 | ||
|
1c5bce8afa | ||
|
8b5997691e | ||
|
35360e2069 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
👉 Delete this line if you have read and agree our pull request rules and guidelines: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
||||||
Fixes #(issue)
|
Fixes #(issue)
|
||||||
|
12
.github/workflows/auto-test.yml
vendored
12
.github/workflows/auto-test.yml
vendored
@@ -11,12 +11,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node: [14, 16, 17]
|
node: [ 14, 16, 17, 18 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -28,7 +30,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm run install-legacy
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
env:
|
env:
|
||||||
@@ -41,10 +43,10 @@ jobs:
|
|||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use Node.js LTS
|
- name: Use Node.js 14
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 14
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm run install-legacy
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"declaration-empty-line-before": null,
|
"declaration-empty-line-before": null,
|
||||||
"alpha-value-notation": "number",
|
"alpha-value-notation": "number",
|
||||||
"color-function-notation": "legacy",
|
"color-function-notation": "legacy",
|
||||||
"shorthand-property-no-redundant-values": null
|
"shorthand-property-no-redundant-values": null,
|
||||||
|
"color-hex-length": null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,17 +27,30 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
|||||||
|
|
||||||
## Can I create a pull request for Uptime Kuma?
|
## Can I create a pull request for Uptime Kuma?
|
||||||
|
|
||||||
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
|
Yes, you can. However, since I don't want to waste your time, be sure to **create empty draft pull request, so we can discuss first** if it is a large pull request or you don't know it will be merged or not.
|
||||||
|
|
||||||
|
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||||
|
|
||||||
|
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||||
|
|
||||||
✅ Accept:
|
✅ Accept:
|
||||||
- Bug/Security fix
|
- Bug/Security fix
|
||||||
- Translations
|
- Translations
|
||||||
- Adding notification providers
|
- Adding notification providers
|
||||||
|
|
||||||
⚠️ Discuss First
|
⚠️ Discussion First
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New features
|
- New features
|
||||||
|
|
||||||
|
❌ Won't Merge
|
||||||
|
- Do not pass auto test
|
||||||
|
- Any breaking changes
|
||||||
|
- Duplicated pull request
|
||||||
|
- Buggy
|
||||||
|
- Existing logic is completely modified or deleted for no reason
|
||||||
|
- A function that is completely out of scope
|
||||||
|
|
||||||
|
|
||||||
### Recommended Pull Request Guideline
|
### Recommended Pull Request Guideline
|
||||||
|
|
||||||
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
@@ -53,22 +66,15 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
|
|||||||
1. Click "Change to draft"
|
1. Click "Change to draft"
|
||||||
1. Discussion
|
1. Discussion
|
||||||
|
|
||||||
#### ❌ Won't Merge
|
|
||||||
|
|
||||||
- Any breaking changes
|
|
||||||
- Duplicated pull request
|
|
||||||
- Buggy
|
|
||||||
- Existing logic is completely modified or deleted
|
|
||||||
- A function that is completely out of scope
|
|
||||||
|
|
||||||
## Project Styles
|
## 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.
|
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||||
|
|
||||||
- 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
|
- 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-compose file. Just map the volume and expose the port, then good to go
|
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
- The web UI styling should be consistent and nice.
|
||||||
|
|
||||||
## Coding Styles
|
## Coding Styles
|
||||||
|
|
||||||
@@ -80,8 +86,8 @@ I personally do not like something need to learn so much and need to config so m
|
|||||||
## Name convention
|
## Name convention
|
||||||
|
|
||||||
- Javascript/Typescript: camelCaseType
|
- Javascript/Typescript: camelCaseType
|
||||||
- SQLite: underscore_type
|
- SQLite: snake_case (Underscore)
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: kebab-case (Dash)
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
|||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||||
* 20 second intervals.
|
* 20 second intervals.
|
||||||
@@ -151,9 +151,9 @@ You can discuss or ask for help in [issues](https://github.com/louislam/uptime-k
|
|||||||
|
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
https://www.reddit.com/r/UptimeKuma/
|
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
@@ -8,9 +8,6 @@ Do not use the issue tracker or discuss it in the public as it will cause more d
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
### Uptime Kuma Versions
|
### Uptime Kuma Versions
|
||||||
|
|
||||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||||
|
@@ -1,18 +1,35 @@
|
|||||||
import legacy from "@vitejs/plugin-legacy";
|
import legacy from "@vitejs/plugin-legacy";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
|
|
||||||
|
const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: [ "ie > 11" ],
|
targets: [ "since 2015" ],
|
||||||
additionalLegacyPolyfills: [ "regenerator-runtime/runtime" ]
|
}),
|
||||||
})
|
visualizer({
|
||||||
|
filename: "tmp/dist-stats.html"
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "gzip",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: "brotliCompress",
|
||||||
|
filter: viteCompressionFilter,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
@@ -21,4 +38,13 @@ export default defineConfig({
|
|||||||
"plugins": [ postcssRTLCSS ]
|
"plugins": [ postcssRTLCSS ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
5
db/patch-add-clickable-status-page-link.sql
Normal file
5
db/patch-add-clickable-status-page-link.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE monitor_group
|
||||||
|
ADD send_url BOOLEAN DEFAULT 0 NOT NULL;
|
||||||
|
COMMIT;
|
18
db/patch-add-docker-columns.sql
Normal file
18
db/patch-add-docker-columns.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE docker_host (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
docker_daemon VARCHAR(255),
|
||||||
|
docker_type VARCHAR(255),
|
||||||
|
name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_host INTEGER REFERENCES docker_host(id);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD docker_container VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-add-other-auth.sql
Normal file
18
db/patch-add-other-auth.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_method VARCHAR(250);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD auth_domain TEXT;
|
||||||
|
ALTER TABLE monitor
|
||||||
|
|
||||||
|
ADD auth_workstation TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
UPDATE monitor
|
||||||
|
SET auth_method = 'basic'
|
||||||
|
WHERE basic_auth_user is not null;
|
||||||
|
COMMIT;
|
18
db/patch-add-radius-monitor.sql
Normal file
18
db/patch-add-radius-monitor.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_username VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_password VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_calling_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_called_station_id VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD radius_secret VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT
|
10
db/patch-add-sqlserver-monitor.sql
Normal file
10
db/patch-add-sqlserver-monitor.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_connection_string VARCHAR(2000);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD database_query TEXT;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT
|
10
db/patch-monitor-add-resend-interval.sql
Normal file
10
db/patch-monitor-add-resend-interval.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 resend_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
ALTER TABLE heartbeat
|
||||||
|
ADD down_count INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
@@ -4,5 +4,5 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
@@ -11,8 +11,9 @@ WORKDIR /app
|
|||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
sqlite3 iputils-ping util-linux dumb-init && \
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
pip3 --no-cache-dir install apprise==1.0.0 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
# Install cloudflared
|
# Install cloudflared
|
||||||
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
@@ -22,5 +23,6 @@ RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
|||||||
apt update && \
|
apt update && \
|
||||||
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
rm -f cloudflared.deb
|
rm -f cloudflared.deb && \
|
||||||
|
apt --yes autoremove
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Simple docker-composer.yml
|
# Simple docker-compose.yml
|
||||||
# You can change your port or volume location
|
# You can change your port or volume location
|
||||||
|
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
image: louislam/uptime-kuma:1
|
image: louislam/uptime-kuma:1
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
volumes:
|
volumes:
|
||||||
- ./uptime-kuma:/app/data
|
- ./uptime-kuma-data:/app/data
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001 # <Host Port>:<Container Port>
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -41,7 +41,7 @@ function updateWiki(newVersion) {
|
|||||||
|
|
||||||
function safeDelete(dir) {
|
function safeDelete(dir) {
|
||||||
if (fs.existsSync(dir)) {
|
if (fs.existsSync(dir)) {
|
||||||
fs.rmdirSync(dir, {
|
fs.rm(dir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
13452
package-lock.json
generated
13452
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
119
package.json
119
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.15.1",
|
"version": "1.18.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"node": "14.* || >=16.*"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install",
|
||||||
"update-legacy": "npm update --legacy-peer-deps",
|
"update-legacy": "npm update",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.18.0 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
@@ -57,30 +57,29 @@
|
|||||||
"ncu-patch": "npm-check-updates -u -t patch",
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
"git-remove-tag": "git tag -d"
|
"git-remove-tag": "git tag -d",
|
||||||
|
"build-dist-and-restart": "npm run build && npm run start-server-dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@louislam/sqlite3": "~15.0.6",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
|
||||||
"@louislam/sqlite3": "~15.0.3",
|
|
||||||
"@popperjs/core": "~2.10.2",
|
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.26.1",
|
"axios": "~0.26.1",
|
||||||
|
"axios-ntlm": "^1.3.0",
|
||||||
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
|
||||||
"bree": "~7.1.5",
|
"bree": "~7.1.5",
|
||||||
|
"cacheable-lookup": "~6.0.4",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~3.6.2",
|
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
|
||||||
"check-password-strength": "^2.0.5",
|
"check-password-strength": "^2.0.5",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
|
"chroma-js": "^2.1.2",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"dayjs": "~1.10.8",
|
"compression": "^1.7.4",
|
||||||
|
"dayjs": "^1.11.0",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"favico.js": "^0.3.10",
|
"express-static-gzip": "^2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "^5.0.0",
|
"http-proxy-agent": "^5.0.0",
|
||||||
@@ -90,25 +89,66 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"limiter": "^2.1.0",
|
"limiter": "^2.1.0",
|
||||||
"mqtt": "^4.2.8",
|
"mqtt": "^4.2.8",
|
||||||
|
"mssql": "^8.1.0",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-radius-client": "^1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
|
"pg": "^8.7.3",
|
||||||
|
"pg-connection-string": "^2.5.0",
|
||||||
|
"prom-client": "~13.2.0",
|
||||||
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"redbean-node": "0.1.4",
|
||||||
|
"socket.io": "~4.4.1",
|
||||||
|
"socket.io-client": "~4.4.1",
|
||||||
|
"socks-proxy-agent": "6.1.1",
|
||||||
|
"tar": "^6.1.11",
|
||||||
|
"tcp-ping": "~0.1.1",
|
||||||
|
"thirty-two": "~1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@actions/github": "~5.0.1",
|
||||||
|
"@babel/eslint-parser": "~7.17.0",
|
||||||
|
"@babel/preset-env": "^7.15.8",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
|
"@popperjs/core": "~2.10.2",
|
||||||
|
"@types/bootstrap": "~5.1.9",
|
||||||
|
"@vitejs/plugin-legacy": "~1.8.2",
|
||||||
|
"@vitejs/plugin-vue": "~2.3.3",
|
||||||
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
|
"aedes": "^0.46.3",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
|
"bootstrap": "5.1.3",
|
||||||
|
"chart.js": "~3.6.2",
|
||||||
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
|
"core-js": "~3.18.3",
|
||||||
|
"cross-env": "~7.0.3",
|
||||||
|
"dns2": "~2.0.1",
|
||||||
|
"eslint": "~8.14.0",
|
||||||
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
|
"favico.js": "^0.3.10",
|
||||||
|
"jest": "~27.2.5",
|
||||||
|
"jest-puppeteer": "~6.0.3",
|
||||||
|
"postcss-html": "^1.3.1",
|
||||||
"postcss-rtlcss": "~3.4.1",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"postcss-scss": "~4.0.3",
|
"postcss-scss": "~4.0.3",
|
||||||
"prismjs": "^1.27.0",
|
"prismjs": "^1.27.0",
|
||||||
"prom-client": "~13.2.0",
|
"puppeteer": "~13.1.3",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
|
||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"redbean-node": "0.1.3",
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"socket.io": "~4.4.1",
|
"sass": "~1.42.1",
|
||||||
"socket.io-client": "~4.4.1",
|
"stylelint": "~14.7.1",
|
||||||
"socks-proxy-agent": "^6.1.1",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"tar": "^6.1.11",
|
|
||||||
"tcp-ping": "~0.1.1",
|
|
||||||
"thirty-two": "~1.0.2",
|
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
|
"vite": "~2.9.9",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "next",
|
"vue": "next",
|
||||||
"vue-chart-3": "3.0.9",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
@@ -120,34 +160,7 @@
|
|||||||
"vue-qrcode": "~1.0.0",
|
"vue-qrcode": "~1.0.0",
|
||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0"
|
"vuedraggable": "~4.1.0",
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@actions/github": "~5.0.1",
|
|
||||||
"@babel/eslint-parser": "~7.17.0",
|
|
||||||
"@babel/preset-env": "^7.15.8",
|
|
||||||
"@types/bootstrap": "~5.1.9",
|
|
||||||
"@vitejs/plugin-legacy": "~1.6.4",
|
|
||||||
"@vitejs/plugin-vue": "~1.9.4",
|
|
||||||
"@vue/compiler-sfc": "~3.2.31",
|
|
||||||
"aedes": "^0.46.3",
|
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"concurrently": "^7.1.0",
|
|
||||||
"core-js": "~3.18.3",
|
|
||||||
"cross-env": "~7.0.3",
|
|
||||||
"dns2": "~2.0.1",
|
|
||||||
"eslint": "~8.14.0",
|
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
|
||||||
"jest": "~27.2.5",
|
|
||||||
"jest-puppeteer": "~6.0.3",
|
|
||||||
"npm-check-updates": "^12.5.9",
|
|
||||||
"postcss-html": "^1.3.1",
|
|
||||||
"puppeteer": "~13.1.3",
|
|
||||||
"sass": "~1.42.1",
|
|
||||||
"stylelint": "~14.7.1",
|
|
||||||
"stylelint-config-standard": "~25.0.0",
|
|
||||||
"typescript": "~4.4.4",
|
|
||||||
"vite": "~2.6.14",
|
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 893 B |
54
server/cacheable-dns-http-agent.js
Normal file
54
server/cacheable-dns-http-agent.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const CacheableLookup = require("cacheable-lookup");
|
||||||
|
|
||||||
|
class CacheableDnsHttpAgent {
|
||||||
|
|
||||||
|
static cacheable = new CacheableLookup();
|
||||||
|
|
||||||
|
static httpAgentList = {};
|
||||||
|
static httpsAgentList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register cacheable to global agents
|
||||||
|
*/
|
||||||
|
static registerGlobalAgent() {
|
||||||
|
this.cacheable.install(http.globalAgent);
|
||||||
|
this.cacheable.install(https.globalAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static install(agent) {
|
||||||
|
this.cacheable.install(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {https.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agent}
|
||||||
|
*/
|
||||||
|
static getHttpsAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpsAgentList)) {
|
||||||
|
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpsAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpsAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var {http.AgentOptions} agentOptions
|
||||||
|
* @return {https.Agents}
|
||||||
|
*/
|
||||||
|
static getHttpAgent(agentOptions) {
|
||||||
|
let key = JSON.stringify(agentOptions);
|
||||||
|
if (!(key in this.httpAgentList)) {
|
||||||
|
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||||
|
this.cacheable.install(this.httpAgentList[key]);
|
||||||
|
}
|
||||||
|
return this.httpAgentList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CacheableDnsHttpAgent,
|
||||||
|
};
|
@@ -22,7 +22,10 @@ async function sendNotificationList(socket) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
result.push(bean.export());
|
let notificationObject = bean.export();
|
||||||
|
notificationObject.isDefault = (notificationObject.isDefault === 1);
|
||||||
|
notificationObject.active = (notificationObject.active === 1);
|
||||||
|
result.push(notificationObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
io.to(socket.userID).emit("notificationList", result);
|
io.to(socket.userID).emit("notificationList", result);
|
||||||
@@ -122,10 +125,35 @@ async function sendInfo(socket) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of docker hosts to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<Bean[]>}
|
||||||
|
*/
|
||||||
|
async function sendDockerHostList(socket) {
|
||||||
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
let list = await R.find("docker_host", " user_id = ? ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of list) {
|
||||||
|
result.push(bean.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("dockerHostList", result);
|
||||||
|
|
||||||
|
timeLogger.print("Send Docker Host List");
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
sendHeartbeatList,
|
sendHeartbeatList,
|
||||||
sendProxyList,
|
sendProxyList,
|
||||||
sendInfo,
|
sendInfo,
|
||||||
|
sendDockerHostList
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,20 @@
|
|||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const demoMode = args["demo"] || false;
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
|
const badgeConstants = {
|
||||||
|
naColor: "#999",
|
||||||
|
defaultUpColor: "#66c20a",
|
||||||
|
defaultDownColor: "#c2290a",
|
||||||
|
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||||
|
defaultStyle: "flat",
|
||||||
|
defaultPingValueSuffix: "ms",
|
||||||
|
defaultPingLabelSuffix: "h",
|
||||||
|
defaultUptimeValueSuffix: "%",
|
||||||
|
defaultUptimeLabelSuffix: "h",
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
args,
|
args,
|
||||||
demoMode
|
demoMode,
|
||||||
|
badgeConstants,
|
||||||
};
|
};
|
||||||
|
@@ -53,11 +53,17 @@ class Database {
|
|||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-add-docker-columns.sql": true,
|
||||||
"patch-status-page.sql": true,
|
"patch-status-page.sql": true,
|
||||||
"patch-proxy.sql": true,
|
"patch-proxy.sql": true,
|
||||||
"patch-monitor-expiry-notification.sql": true,
|
"patch-monitor-expiry-notification.sql": true,
|
||||||
"patch-status-page-footer-css.sql": true,
|
"patch-status-page-footer-css.sql": true,
|
||||||
"patch-added-mqtt-monitor.sql": true,
|
"patch-added-mqtt-monitor.sql": true,
|
||||||
|
"patch-add-clickable-status-page-link.sql": true,
|
||||||
|
"patch-add-sqlserver-monitor.sql": true,
|
||||||
|
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||||
|
"patch-add-radius-monitor.sql": true,
|
||||||
|
"patch-monitor-add-resend-interval.sql": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +150,9 @@ class Database {
|
|||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
|
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
|
||||||
|
await R.exec("PRAGMA busy_timeout = 5000");
|
||||||
|
|
||||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
// FULL synchronous is very safe, but it is also slower.
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
@@ -175,7 +184,13 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
log.info("db", "Database patch is needed");
|
log.info("db", "Database patch is needed");
|
||||||
|
|
||||||
this.backup(version);
|
try {
|
||||||
|
this.backup(version);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("db", e);
|
||||||
|
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Try catch anything here, if gone wrong, restore the backup
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
try {
|
try {
|
||||||
@@ -443,6 +458,23 @@ class Database {
|
|||||||
this.backupWalPath = walPath + ".bak" + version;
|
this.backupWalPath = walPath + ".bak" + version;
|
||||||
fs.copyFileSync(walPath, this.backupWalPath);
|
fs.copyFileSync(walPath, this.backupWalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double confirm if all files actually backup
|
||||||
|
if (!fs.existsSync(this.backupPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
if (!fs.existsSync(this.backupShmPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupShmPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
if (!fs.existsSync(this.backupWalPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupWalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
106
server/docker.js
Normal file
106
server/docker.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const version = require("../package.json").version;
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
class DockerHost {
|
||||||
|
/**
|
||||||
|
* Save a docker host
|
||||||
|
* @param {Object} dockerHost Docker host to save
|
||||||
|
* @param {?number} dockerHostID ID of the docker host to update
|
||||||
|
* @param {number} userID ID of the user who adds the docker host
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async save(dockerHost, dockerHostID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (dockerHostID) {
|
||||||
|
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("docker_host");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.docker_daemon = dockerHost.dockerDaemon;
|
||||||
|
bean.docker_type = dockerHost.dockerType;
|
||||||
|
bean.name = dockerHost.name;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Docker host
|
||||||
|
* @param {number} dockerHostID ID of the Docker host to delete
|
||||||
|
* @param {number} userID ID of the user who created the Docker host
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(dockerHostID, userID) {
|
||||||
|
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("docker host not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed proxy from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
|
||||||
|
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the amount of containers on the Docker host
|
||||||
|
* @param {Object} dockerHost Docker host to check for
|
||||||
|
* @returns {number} Total amount of containers on the host
|
||||||
|
*/
|
||||||
|
static async testDockerHost(dockerHost) {
|
||||||
|
const options = {
|
||||||
|
url: "/containers/json?all=true",
|
||||||
|
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: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost.dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost.dockerDaemon;
|
||||||
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
|
options.baseURL = dockerHost.dockerDaemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await axios.request(options);
|
||||||
|
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
|
||||||
|
if (res.data.length > 1) {
|
||||||
|
|
||||||
|
if ("ImageID" in res.data[0]) {
|
||||||
|
return res.data.length;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return res.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DockerHost,
|
||||||
|
};
|
19
server/model/docker_host.js
Normal file
19
server/model/docker_host.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class DockerHost extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Returns an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
userID: this.user_id,
|
||||||
|
dockerDaemon: this.docker_daemon,
|
||||||
|
dockerType: this.docker_type,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DockerHost;
|
@@ -31,7 +31,7 @@ class Group extends BeanModel {
|
|||||||
*/
|
*/
|
||||||
async getMonitorList() {
|
async getMonitorList() {
|
||||||
return R.convertToBeans("monitor", await R.getAll(`
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
SELECT monitor.* FROM monitor, monitor_group
|
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
|
||||||
WHERE monitor.id = monitor_group.monitor_id
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
AND group_id = ?
|
AND group_id = ?
|
||||||
ORDER BY monitor_group.weight
|
ORDER BY monitor_group.weight
|
||||||
|
@@ -7,7 +7,7 @@ dayjs.extend(timezone);
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog, mqttAsync } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
@@ -15,6 +15,8 @@ const { Proxy } = require("../proxy");
|
|||||||
const { demoMode } = require("../config");
|
const { demoMode } = require("../config");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -33,7 +35,13 @@ class Monitor extends BeanModel {
|
|||||||
let obj = {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
sendUrl: this.sendUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.sendUrl) {
|
||||||
|
obj.url = this.url;
|
||||||
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
@@ -71,6 +79,7 @@ class Monitor extends BeanModel {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
|
resendInterval: this.resendInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
expiryNotification: this.isEnabledExpiryNotification(),
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
@@ -80,13 +89,26 @@ class Monitor extends BeanModel {
|
|||||||
dns_resolve_type: this.dns_resolve_type,
|
dns_resolve_type: this.dns_resolve_type,
|
||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
|
pushToken: this.pushToken,
|
||||||
|
docker_container: this.docker_container,
|
||||||
|
docker_host: this.docker_host,
|
||||||
proxyId: this.proxy_id,
|
proxyId: this.proxy_id,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
mqttUsername: this.mqttUsername,
|
mqttUsername: this.mqttUsername,
|
||||||
mqttPassword: this.mqttPassword,
|
mqttPassword: this.mqttPassword,
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
mqttSuccessMessage: this.mqttSuccessMessage
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
|
databaseQuery: this.databaseQuery,
|
||||||
|
authMethod: this.authMethod,
|
||||||
|
authWorkstation: this.authWorkstation,
|
||||||
|
authDomain: this.authDomain,
|
||||||
|
radiusUsername: this.radiusUsername,
|
||||||
|
radiusPassword: this.radiusPassword,
|
||||||
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
|
radiusCallingStationId: this.radiusCallingStationId,
|
||||||
|
radiusSecret: this.radiusSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@@ -181,7 +203,7 @@ class Monitor extends BeanModel {
|
|||||||
// undefined if not https
|
// undefined if not https
|
||||||
let tlsInfo = undefined;
|
let tlsInfo = undefined;
|
||||||
|
|
||||||
if (!previousBeat) {
|
if (!previousBeat || this.type === "push") {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
@@ -191,8 +213,9 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
bean.downCount = previousBeat?.downCount || 0;
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
@@ -212,7 +235,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
// HTTP basic auth
|
// HTTP basic auth
|
||||||
let basicAuthHeader = {};
|
let basicAuthHeader = {};
|
||||||
if (this.basic_auth_user) {
|
if (this.auth_method === "basic") {
|
||||||
basicAuthHeader = {
|
basicAuthHeader = {
|
||||||
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
"Authorization": "Basic " + this.encodeBase64(this.basic_auth_user, this.basic_auth_pass),
|
||||||
};
|
};
|
||||||
@@ -263,7 +286,21 @@ class Monitor extends BeanModel {
|
|||||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
|
|
||||||
let res = await axios.request(options);
|
let res;
|
||||||
|
if (this.auth_method === "ntlm") {
|
||||||
|
options.httpsAgent.keepAlive = true;
|
||||||
|
|
||||||
|
res = await httpNtlm(options, {
|
||||||
|
username: this.basic_auth_user,
|
||||||
|
password: this.basic_auth_pass,
|
||||||
|
domain: this.authDomain,
|
||||||
|
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res = await axios.request(options);
|
||||||
|
}
|
||||||
|
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
@@ -311,7 +348,11 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg += ", keyword is found";
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found");
|
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||||
|
if (data.length > 50) {
|
||||||
|
data = data.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -329,7 +370,7 @@ class Monitor extends BeanModel {
|
|||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
let dnsMessage = "";
|
let dnsMessage = "";
|
||||||
|
|
||||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
|
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||||
@@ -366,25 +407,33 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = dnsMessage;
|
bean.msg = dnsMessage;
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "push") { // Type: Push
|
} else if (this.type === "push") { // Type: Push
|
||||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
|
const bufferTime = 1000; // 1s buffer to accommodate clock differences
|
||||||
|
|
||||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
if (previousBeat) {
|
||||||
this.id,
|
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
|
||||||
time
|
|
||||||
]);
|
|
||||||
|
|
||||||
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
|
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
|
||||||
|
|
||||||
if (heartbeatCount <= 0) {
|
// If the previous beat was down or pending we use the regular
|
||||||
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
||||||
|
throw new Error("No heartbeat in the time window");
|
||||||
throw new Error("No heartbeat in the time window");
|
} else {
|
||||||
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
if (timeout < 0) {
|
||||||
|
timeout = bufferTime;
|
||||||
|
} else {
|
||||||
|
timeout += bufferTime;
|
||||||
|
}
|
||||||
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
|
retries = 0;
|
||||||
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
|
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No need to insert successful heartbeat for push type, so end here
|
throw new Error("No heartbeat in the time window");
|
||||||
retries = 0;
|
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "steam") {
|
} else if (this.type === "steam") {
|
||||||
@@ -402,10 +451,13 @@ class Monitor extends BeanModel {
|
|||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
}),
|
}),
|
||||||
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
|
maxCachedSessions: 0,
|
||||||
|
}),
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
@@ -426,6 +478,35 @@ class Monitor extends BeanModel {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("Server not found on Steam");
|
throw new Error("Server not found on Steam");
|
||||||
}
|
}
|
||||||
|
} else if (this.type === "docker") {
|
||||||
|
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||||
|
|
||||||
|
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url: `/containers/${this.docker_container}/json`,
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dockerHost._dockerType === "socket") {
|
||||||
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
|
options.baseURL = dockerHost._dockerDaemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`[${this.name}] Axios Request`);
|
||||||
|
let res = await axios.request(options);
|
||||||
|
if (res.data.State.Running) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = "";
|
||||||
|
}
|
||||||
} else if (this.type === "mqtt") {
|
} else if (this.type === "mqtt") {
|
||||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||||
port: this.port,
|
port: this.port,
|
||||||
@@ -434,6 +515,46 @@ class Monitor extends BeanModel {
|
|||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
});
|
});
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
} else if (this.type === "sqlserver") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "postgres") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "radius") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
try {
|
||||||
|
const resp = await radius(
|
||||||
|
this.hostname,
|
||||||
|
this.radiusUsername,
|
||||||
|
this.radiusPassword,
|
||||||
|
this.radiusCalledStationId,
|
||||||
|
this.radiusCallingStationId,
|
||||||
|
this.radiusSecret
|
||||||
|
);
|
||||||
|
if (resp.code) {
|
||||||
|
bean.msg = resp.code;
|
||||||
|
}
|
||||||
|
bean.status = UP;
|
||||||
|
} catch (error) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
if (error.response?.code) {
|
||||||
|
bean.msg = error.response.code;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
@@ -475,23 +596,38 @@ class Monitor extends BeanModel {
|
|||||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
|
||||||
// Clear Status Page Cache
|
// Clear Status Page Cache
|
||||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
|
|
||||||
|
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||||
|
++bean.downCount;
|
||||||
|
if (bean.downCount >= this.resendInterval) {
|
||||||
|
// Send notification again, because we are still DOWN
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
log.info("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else if (bean.status === PENDING) {
|
} else if (bean.status === PENDING) {
|
||||||
if (this.retryInterval > 0) {
|
if (this.retryInterval > 0) {
|
||||||
beatInterval = this.retryInterval;
|
beatInterval = this.retryInterval;
|
||||||
}
|
}
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Send to socket`);
|
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||||
@@ -521,7 +657,7 @@ class Monitor extends BeanModel {
|
|||||||
await beat();
|
await beat();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e);
|
console.trace(e);
|
||||||
errorLog(e, false);
|
UptimeKumaServer.errorLog(e, false);
|
||||||
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
@@ -828,10 +964,19 @@ class Monitor extends BeanModel {
|
|||||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||||
const notificationList = await Monitor.getNotificationList(this);
|
const notificationList = await Monitor.getNotificationList(this);
|
||||||
|
|
||||||
log.debug("monitor", "call sendCertNotificationByTargetDays");
|
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
|
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
|
// Reset Default
|
||||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
|
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||||
|
notifyDays = [ 7, 14, 21 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||||
|
for (const day of notifyDays) {
|
||||||
|
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||||
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +1,109 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const cheerio = require("cheerio");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
static domainMappingList = { };
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Response} response
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {string} slug
|
||||||
|
*/
|
||||||
|
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (statusPage) {
|
||||||
|
response.send(await StatusPage.renderHTML(indexHTML, statusPage));
|
||||||
|
} else {
|
||||||
|
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSR for status pages
|
||||||
|
* @param {string} indexHTML
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async renderHTML(indexHTML, statusPage) {
|
||||||
|
const $ = cheerio.load(indexHTML);
|
||||||
|
const description155 = statusPage.description?.substring(0, 155);
|
||||||
|
|
||||||
|
$("title").text(statusPage.title);
|
||||||
|
$("meta[name=description]").attr("content", description155);
|
||||||
|
|
||||||
|
if (statusPage.icon) {
|
||||||
|
$("link[rel=icon]")
|
||||||
|
.attr("href", statusPage.icon)
|
||||||
|
.removeAttr("type");
|
||||||
|
|
||||||
|
$("link[rel=apple-touch-icon]").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const head = $("head");
|
||||||
|
|
||||||
|
// OG Meta Tags
|
||||||
|
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||||
|
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||||
|
|
||||||
|
// Preload data
|
||||||
|
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
|
||||||
|
head.append(`
|
||||||
|
<script>
|
||||||
|
window.preloadData = ${json}
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// manifest.json
|
||||||
|
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
|
||||||
|
|
||||||
|
return $.root().html();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all status page data in one call
|
||||||
|
* @param {StatusPage} statusPage
|
||||||
|
*/
|
||||||
|
static async getStatusPageData(statusPage) {
|
||||||
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (incident) {
|
||||||
|
incident = incident.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Group List
|
||||||
|
const publicGroupList = [];
|
||||||
|
const showTags = !!statusPage.show_tags;
|
||||||
|
|
||||||
|
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||||
|
statusPage.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||||
|
publicGroupList.push(monitorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
return {
|
||||||
|
config: await statusPage.toPublicJSON(),
|
||||||
|
incident,
|
||||||
|
publicGroupList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads domain mapping from DB
|
* Loads domain mapping from DB
|
||||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
50
server/notification-providers/alertnow.js
Normal file
50
server/notification-providers/alertnow.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||||
|
|
||||||
|
class AlertNow extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "AlertNow";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let textMsg = "";
|
||||||
|
let status = "open";
|
||||||
|
let eventType = "ERROR";
|
||||||
|
let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||||
|
|
||||||
|
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`;
|
||||||
|
status = "close";
|
||||||
|
eventType = "INFO";
|
||||||
|
eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`;
|
||||||
|
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||||
|
textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`;
|
||||||
|
}
|
||||||
|
|
||||||
|
textMsg += ` - ${msg}`;
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorJSON) {
|
||||||
|
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"summary": textMsg,
|
||||||
|
"status": status,
|
||||||
|
"event_type": eventType,
|
||||||
|
"event_id": eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(notification.alertNowWebhookURL, data);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AlertNow;
|
@@ -93,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
|
|||||||
param2[key] = param[key];
|
param2[key] = param[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape more characters than encodeURIComponent does.
|
||||||
|
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
|
||||||
|
// See https://help.aliyun.com/document_detail/315526.html
|
||||||
|
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
|
||||||
|
let moreEscapesTable = function (m) {
|
||||||
|
return {
|
||||||
|
"!": "%21",
|
||||||
|
"*": "%2A",
|
||||||
|
"'": "%27",
|
||||||
|
"(": "%28",
|
||||||
|
")": "%29"
|
||||||
|
}[m];
|
||||||
|
};
|
||||||
|
|
||||||
for (let key in param2) {
|
for (let key in param2) {
|
||||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
|
||||||
|
data.push(`${encodeURIComponent(key)}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||||
|
@@ -6,9 +6,14 @@ class Apprise extends NotificationProvider {
|
|||||||
name = "apprise";
|
name = "apprise";
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let s = childProcess.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL ]);
|
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||||
|
if (notification.title) {
|
||||||
|
args.push("-t");
|
||||||
|
args.push(notification.title);
|
||||||
|
}
|
||||||
|
const s = childProcess.spawnSync("apprise", args);
|
||||||
|
|
||||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
||||||
|
@@ -12,9 +12,7 @@ const { default: axios } = require("axios");
|
|||||||
|
|
||||||
// bark is an APN bridge that sends notifications to Apple devices.
|
// bark is an APN bridge that sends notifications to Apple devices.
|
||||||
|
|
||||||
const barkNotificationGroup = "UptimeKuma";
|
|
||||||
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||||
const barkNotificationSound = "telegraph";
|
|
||||||
const successMessage = "Successes!";
|
const successMessage = "Successes!";
|
||||||
|
|
||||||
class Bark extends NotificationProvider {
|
class Bark extends NotificationProvider {
|
||||||
@@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
|
|||||||
* @param {string} postUrl URL to append parameters to
|
* @param {string} postUrl URL to append parameters to
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
appendAdditionalParameters(postUrl) {
|
appendAdditionalParameters(notification, postUrl) {
|
||||||
// grouping all our notifications
|
|
||||||
postUrl += "?group=" + barkNotificationGroup;
|
|
||||||
// set icon to uptime kuma icon, 11kb should be fine
|
// set icon to uptime kuma icon, 11kb should be fine
|
||||||
postUrl += "&icon=" + barkNotificationAvatar;
|
postUrl += "&icon=" + barkNotificationAvatar;
|
||||||
|
// grouping all our notifications
|
||||||
|
if (notification.barkGroup != null) {
|
||||||
|
postUrl += "&group=" + notification.barkGroup;
|
||||||
|
} else {
|
||||||
|
// default name
|
||||||
|
postUrl += "&group=" + "UptimeKuma";
|
||||||
|
}
|
||||||
// picked a sound, this should follow system's mute status when arrival
|
// picked a sound, this should follow system's mute status when arrival
|
||||||
postUrl += "&sound=" + barkNotificationSound;
|
if (notification.barkSound != null) {
|
||||||
|
postUrl += "&sound=" + notification.barkSound;
|
||||||
|
} else {
|
||||||
|
// default sound
|
||||||
|
postUrl += "&sound=" + "telegraph";
|
||||||
|
}
|
||||||
return postUrl;
|
return postUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,16 +22,23 @@ class Discord extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
let address;
|
||||||
|
|
||||||
if (monitorJSON["type"] === "port") {
|
switch (monitorJSON["type"]) {
|
||||||
url = monitorJSON["hostname"];
|
case "ping":
|
||||||
if (monitorJSON["port"]) {
|
address = monitorJSON["hostname"];
|
||||||
url += ":" + monitorJSON["port"];
|
break;
|
||||||
}
|
case "port":
|
||||||
|
case "dns":
|
||||||
} else {
|
case "steam":
|
||||||
url = monitorJSON["url"];
|
address = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
address += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
address = monitorJSON["url"];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||||
@@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@@ -83,8 +90,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["name"],
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Time (UTC)",
|
||||||
@@ -92,7 +99,7 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ping",
|
name: "Ping",
|
||||||
value: heartbeatJSON["ping"] + "ms",
|
value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
|
38
server/notification-providers/home-assistant.js
Normal file
38
server/notification-providers/home-assistant.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const defaultNotificationService = "notify";
|
||||||
|
|
||||||
|
class HomeAssistant extends NotificationProvider {
|
||||||
|
name = "HomeAssistant";
|
||||||
|
|
||||||
|
async send(notification, message, monitor = null, heartbeat = null) {
|
||||||
|
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||||
|
{
|
||||||
|
title: "Uptime Kuma",
|
||||||
|
message,
|
||||||
|
...(notificationService !== "persistent_notification" && { data: {
|
||||||
|
name: monitor?.name,
|
||||||
|
status: heartbeat?.status,
|
||||||
|
} }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return "Sent Successfully.";
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HomeAssistant;
|
43
server/notification-providers/linenotify.js
Normal file
43
server/notification-providers/linenotify.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const qs = require("qs");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class LineNotify extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "LineNotify";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": "Bearer " + notification.lineNotifyAccessToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let testMessage = {
|
||||||
|
"message": msg,
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
|
let downMessage = {
|
||||||
|
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
let upMessage = {
|
||||||
|
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
|
};
|
||||||
|
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||||
|
}
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LineNotify;
|
@@ -14,7 +14,7 @@ class LunaSea extends NotificationProvider {
|
|||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let testdata = {
|
let testdata = {
|
||||||
"title": "Uptime Kuma Alert",
|
"title": "Uptime Kuma Alert",
|
||||||
"body": "Testing Successful.",
|
"body": msg,
|
||||||
};
|
};
|
||||||
await axios.post(lunaseadevice, testdata);
|
await axios.post(lunaseadevice, testdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
26
server/notification-providers/ntfy.js
Normal file
26
server/notification-providers/ntfy.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Ntfy extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ntfy";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`${notification.ntfyserverurl}`, {
|
||||||
|
"topic": notification.ntfytopic,
|
||||||
|
"message": msg,
|
||||||
|
"priority": notification.ntfyPriority || 4,
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Ntfy;
|
113
server/notification-providers/pagerduty.js
Normal file
113
server/notification-providers/pagerduty.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
|
class PagerDuty extends NotificationProvider {
|
||||||
|
name = "PagerDuty";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const title = "Uptime Kuma Alert";
|
||||||
|
const monitor = {
|
||||||
|
type: "ping",
|
||||||
|
url: "Uptime Kuma Test Button",
|
||||||
|
};
|
||||||
|
return this.postNotification(notification, title, msg, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === UP) {
|
||||||
|
const title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
const eventAction = notification.pagerdutyAutoResolve || null;
|
||||||
|
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === DOWN) {
|
||||||
|
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if result is successful, result code should be in range 2xx
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("PagerDuty notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("PagerDuty notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {BeanModel} notification Message title
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} body Message
|
||||||
|
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||||
|
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||||
|
|
||||||
|
if (eventAction == null) {
|
||||||
|
return "No action required";
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitorUrl;
|
||||||
|
if (monitorInfo.type === "port") {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
if (monitorInfo.port) {
|
||||||
|
monitorUrl += ":" + monitorInfo.port;
|
||||||
|
}
|
||||||
|
} else if (monitorInfo.hostname != null) {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
} else {
|
||||||
|
monitorUrl = monitorInfo.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
url: notification.pagerdutyIntegrationUrl,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
summary: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||||
|
severity: notification.pagerdutyPriority || "warning",
|
||||||
|
source: monitorUrl,
|
||||||
|
},
|
||||||
|
routing_key: notification.pagerdutyIntegrationKey,
|
||||||
|
event_action: eventAction,
|
||||||
|
dedup_key: "Uptime Kuma/" + monitorInfo.id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorInfo) {
|
||||||
|
options.client = "Uptime Kuma";
|
||||||
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await axios.request(options);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "PagerDuty notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PagerDuty;
|
@@ -1,38 +1,43 @@
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
const Alerta = require("./notification-providers/alerta");
|
||||||
|
const AlertNow = require("./notification-providers/alertnow");
|
||||||
|
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||||
const Apprise = require("./notification-providers/apprise");
|
const Apprise = require("./notification-providers/apprise");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Bark = require("./notification-providers/bark");
|
||||||
const Gotify = require("./notification-providers/gotify");
|
|
||||||
const Line = require("./notification-providers/line");
|
|
||||||
const LunaSea = require("./notification-providers/lunasea");
|
|
||||||
const Mattermost = require("./notification-providers/mattermost");
|
|
||||||
const Matrix = require("./notification-providers/matrix");
|
|
||||||
const Octopush = require("./notification-providers/octopush");
|
|
||||||
const PromoSMS = require("./notification-providers/promosms");
|
|
||||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||||
|
const DingDing = require("./notification-providers/dingding");
|
||||||
|
const Discord = require("./notification-providers/discord");
|
||||||
|
const Feishu = require("./notification-providers/feishu");
|
||||||
|
const GoogleChat = require("./notification-providers/google-chat");
|
||||||
|
const Gorush = require("./notification-providers/gorush");
|
||||||
|
const Gotify = require("./notification-providers/gotify");
|
||||||
|
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||||
|
const Line = require("./notification-providers/line");
|
||||||
|
const LineNotify = require("./notification-providers/linenotify");
|
||||||
|
const LunaSea = require("./notification-providers/lunasea");
|
||||||
|
const Matrix = require("./notification-providers/matrix");
|
||||||
|
const Mattermost = require("./notification-providers/mattermost");
|
||||||
|
const Ntfy = require("./notification-providers/ntfy");
|
||||||
|
const Octopush = require("./notification-providers/octopush");
|
||||||
|
const OneBot = require("./notification-providers/onebot");
|
||||||
|
const PagerDuty = require("./notification-providers/pagerduty");
|
||||||
|
const PromoSMS = require("./notification-providers/promosms");
|
||||||
const Pushbullet = require("./notification-providers/pushbullet");
|
const Pushbullet = require("./notification-providers/pushbullet");
|
||||||
|
const PushDeer = require("./notification-providers/pushdeer");
|
||||||
const Pushover = require("./notification-providers/pushover");
|
const Pushover = require("./notification-providers/pushover");
|
||||||
const Pushy = require("./notification-providers/pushy");
|
const Pushy = require("./notification-providers/pushy");
|
||||||
const TechulusPush = require("./notification-providers/techulus-push");
|
|
||||||
const RocketChat = require("./notification-providers/rocket-chat");
|
const RocketChat = require("./notification-providers/rocket-chat");
|
||||||
|
const SerwerSMS = require("./notification-providers/serwersms");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
const SMTP = require("./notification-providers/smtp");
|
const SMTP = require("./notification-providers/smtp");
|
||||||
|
const Stackfield = require("./notification-providers/stackfield");
|
||||||
const Teams = require("./notification-providers/teams");
|
const Teams = require("./notification-providers/teams");
|
||||||
|
const TechulusPush = require("./notification-providers/techulus-push");
|
||||||
const Telegram = require("./notification-providers/telegram");
|
const Telegram = require("./notification-providers/telegram");
|
||||||
const Webhook = require("./notification-providers/webhook");
|
const Webhook = require("./notification-providers/webhook");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
|
||||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
|
||||||
const DingDing = require("./notification-providers/dingding");
|
|
||||||
const Bark = require("./notification-providers/bark");
|
|
||||||
const { log } = require("../src/util");
|
|
||||||
const SerwerSMS = require("./notification-providers/serwersms");
|
|
||||||
const Stackfield = require("./notification-providers/stackfield");
|
|
||||||
const WeCom = require("./notification-providers/wecom");
|
const WeCom = require("./notification-providers/wecom");
|
||||||
const GoogleChat = require("./notification-providers/google-chat");
|
|
||||||
const Gorush = require("./notification-providers/gorush");
|
|
||||||
const Alerta = require("./notification-providers/alerta");
|
|
||||||
const OneBot = require("./notification-providers/onebot");
|
|
||||||
const PushDeer = require("./notification-providers/pushdeer");
|
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
@@ -45,39 +50,44 @@ class Notification {
|
|||||||
this.providerList = {};
|
this.providerList = {};
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
new Apprise(),
|
new Alerta(),
|
||||||
|
new AlertNow(),
|
||||||
new AliyunSms(),
|
new AliyunSms(),
|
||||||
|
new Apprise(),
|
||||||
|
new Bark(),
|
||||||
|
new ClickSendSMS(),
|
||||||
new DingDing(),
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Teams(),
|
|
||||||
new Gotify(),
|
|
||||||
new Line(),
|
|
||||||
new LunaSea(),
|
|
||||||
new Feishu(),
|
new Feishu(),
|
||||||
new Mattermost(),
|
new GoogleChat(),
|
||||||
|
new Gorush(),
|
||||||
|
new Gotify(),
|
||||||
|
new HomeAssistant(),
|
||||||
|
new Line(),
|
||||||
|
new LineNotify(),
|
||||||
|
new LunaSea(),
|
||||||
new Matrix(),
|
new Matrix(),
|
||||||
|
new Mattermost(),
|
||||||
|
new Ntfy(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
|
new OneBot(),
|
||||||
|
new PagerDuty(),
|
||||||
new PromoSMS(),
|
new PromoSMS(),
|
||||||
new ClickSendSMS(),
|
|
||||||
new Pushbullet(),
|
new Pushbullet(),
|
||||||
|
new PushDeer(),
|
||||||
new Pushover(),
|
new Pushover(),
|
||||||
new Pushy(),
|
new Pushy(),
|
||||||
new TechulusPush(),
|
|
||||||
new RocketChat(),
|
new RocketChat(),
|
||||||
|
new SerwerSMS(),
|
||||||
new Signal(),
|
new Signal(),
|
||||||
new Slack(),
|
new Slack(),
|
||||||
new SMTP(),
|
new SMTP(),
|
||||||
|
new Stackfield(),
|
||||||
|
new Teams(),
|
||||||
|
new TechulusPush(),
|
||||||
new Telegram(),
|
new Telegram(),
|
||||||
new Webhook(),
|
new Webhook(),
|
||||||
new Bark(),
|
|
||||||
new SerwerSMS(),
|
|
||||||
new Stackfield(),
|
|
||||||
new WeCom(),
|
new WeCom(),
|
||||||
new GoogleChat(),
|
|
||||||
new Gorush(),
|
|
||||||
new Alerta(),
|
|
||||||
new OneBot(),
|
|
||||||
new PushDeer(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
let express = require("express");
|
let express = require("express");
|
||||||
const { allowDevAllOrigin } = require("../util-server");
|
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
@@ -7,6 +7,9 @@ const dayjs = require("dayjs");
|
|||||||
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { makeBadge } = require("badge-maker");
|
||||||
|
const { badgeConstants } = require("../config");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
@@ -56,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
let duration = 0;
|
let duration = 0;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
|
|
||||||
if (previousHeartbeat) {
|
if (previousHeartbeat) {
|
||||||
isFirstBeat = false;
|
isFirstBeat = false;
|
||||||
@@ -64,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
log.debug("router", "PreviousStatus: " + previousStatus);
|
||||||
log.debug("router", "Current Status: " + status);
|
log.debug("router", "Current Status: " + status);
|
||||||
|
|
||||||
@@ -88,125 +92,188 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.json({
|
response.status(404).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status page config, incident, monitor list
|
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||||
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
allowAllOrigin(response);
|
||||||
allowDevAllOrigin(response);
|
|
||||||
let slug = request.params.slug;
|
|
||||||
|
|
||||||
// Get Status Page
|
const {
|
||||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
label,
|
||||||
slug
|
upLabel = "Up",
|
||||||
]);
|
downLabel = "Down",
|
||||||
|
upColor = badgeConstants.defaultUpColor,
|
||||||
if (!statusPage) {
|
downColor = badgeConstants.defaultDownColor,
|
||||||
response.statusCode = 404;
|
style = badgeConstants.defaultStyle,
|
||||||
response.json({
|
value, // for demo purpose only
|
||||||
msg: "Not Found"
|
} = request.query;
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Incident
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
const overrideValue = value !== undefined ? parseInt(value) : undefined;
|
||||||
statusPage.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (incident) {
|
let publicMonitor = await R.getRow(`
|
||||||
incident = incident.toPublicJSON();
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND monitor_group.monitor_id = ?
|
||||||
|
AND public = 1
|
||||||
|
`,
|
||||||
|
[ requestedMonitorId ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicMonitor) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||||
|
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||||
|
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
|
badgeValues.color = state ? upColor : downColor;
|
||||||
|
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public Group List
|
// build the svg based on given values
|
||||||
const publicGroupList = [];
|
const svg = makeBadge(badgeValues);
|
||||||
const showTags = !!statusPage.show_tags;
|
|
||||||
|
|
||||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
|
||||||
statusPage.id
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let groupBean of list) {
|
|
||||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
|
||||||
publicGroupList.push(monitorGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response
|
|
||||||
response.json({
|
|
||||||
config: await statusPage.toPublicJSON(),
|
|
||||||
incident,
|
|
||||||
publicGroupList
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
send403(response, error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Status Page Polling Data
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let heartbeatList = {};
|
|
||||||
let uptimeList = {};
|
|
||||||
|
|
||||||
let slug = request.params.slug;
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
|
|
||||||
let monitorIDList = await R.getCol(`
|
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
|
||||||
AND public = 1
|
|
||||||
AND \`group\`.status_page_id = ?
|
|
||||||
`, [
|
|
||||||
statusPageID
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
|
||||||
let list = await R.getAll(`
|
|
||||||
SELECT * FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 50
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
list = R.convertToBeans("heartbeat", list);
|
|
||||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
|
||||||
|
|
||||||
const type = 24;
|
|
||||||
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json({
|
|
||||||
heartbeatList,
|
|
||||||
uptimeList
|
|
||||||
});
|
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send403(response, error.message);
|
send403(response, error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => {
|
||||||
* Send a 403 response
|
allowAllOrigin(response);
|
||||||
* @param {Object} res Express response object
|
|
||||||
* @param {string} [msg=""] Message to send
|
const {
|
||||||
*/
|
label,
|
||||||
function send403(res, msg = "") {
|
labelPrefix,
|
||||||
res.status(403).json({
|
labelSuffix = badgeConstants.defaultUptimeLabelSuffix,
|
||||||
"status": "fail",
|
prefix,
|
||||||
"msg": msg,
|
suffix = badgeConstants.defaultUptimeValueSuffix,
|
||||||
});
|
color,
|
||||||
}
|
labelColor,
|
||||||
|
style = badgeConstants.defaultStyle,
|
||||||
|
value, // for demo purpose only
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
|
// if no duration is given, set value to 24 (h)
|
||||||
|
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
||||||
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
let publicMonitor = await R.getRow(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND monitor_group.monitor_id = ?
|
||||||
|
AND public = 1
|
||||||
|
`,
|
||||||
|
[ requestedMonitorId ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicMonitor) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const uptime = overrideValue ?? await Monitor.calcUptime(
|
||||||
|
requestedDuration,
|
||||||
|
requestedMonitorId
|
||||||
|
);
|
||||||
|
|
||||||
|
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||||
|
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||||
|
|
||||||
|
// use a given, custom color or calculate one based on the uptime value
|
||||||
|
badgeValues.color = color ?? percentageToColor(uptime);
|
||||||
|
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||||
|
badgeValues.labelColor = labelColor ?? "";
|
||||||
|
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||||
|
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||||
|
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the SVG based on given values
|
||||||
|
const svg = makeBadge(badgeValues);
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => {
|
||||||
|
allowAllOrigin(response);
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
labelPrefix,
|
||||||
|
labelSuffix = badgeConstants.defaultPingLabelSuffix,
|
||||||
|
prefix,
|
||||||
|
suffix = badgeConstants.defaultPingValueSuffix,
|
||||||
|
color = badgeConstants.defaultPingColor,
|
||||||
|
labelColor,
|
||||||
|
style = badgeConstants.defaultStyle,
|
||||||
|
value, // for demo purpose only
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
|
|
||||||
|
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||||
|
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
||||||
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
const publicAvgPing = parseInt(await R.getCell(`
|
||||||
|
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||||
|
AND heartbeat.ping IS NOT NULL
|
||||||
|
AND public = 1
|
||||||
|
AND heartbeat.monitor_id = ?
|
||||||
|
`,
|
||||||
|
[ -requestedDuration, requestedMonitorId ]
|
||||||
|
));
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!publicAvgPing) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
} else {
|
||||||
|
const avgPing = parseInt(overrideValue ?? publicAvgPing);
|
||||||
|
|
||||||
|
badgeValues.color = color;
|
||||||
|
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||||
|
badgeValues.labelColor = labelColor ?? "";
|
||||||
|
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||||
|
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||||
|
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the SVG based on given values
|
||||||
|
const svg = makeBadge(badgeValues);
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
148
server/routers/status-page-router.js
Normal file
148
server/routers/status-page-router.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
let express = require("express");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
|
const { allowDevAllOrigin, send403 } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const Monitor = require("../model/monitor");
|
||||||
|
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
let cache = apicache.middleware;
|
||||||
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
|
||||||
|
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = request.params.slug;
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/status", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = "default";
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/status-page", cache("5 minutes"), async (request, response) => {
|
||||||
|
let slug = "default";
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status page config, incident, monitor list
|
||||||
|
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPageData = await StatusPage.getStatusPageData(statusPage);
|
||||||
|
|
||||||
|
if (!statusPageData) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
response.json(statusPageData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Polling Data
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let heartbeatList = {};
|
||||||
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let slug = request.params.slug;
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
let monitorIDList = await R.getCol(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND public = 1
|
||||||
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let monitorID of monitorIDList) {
|
||||||
|
let list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 50
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
list = R.convertToBeans("heartbeat", list);
|
||||||
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
|
||||||
|
const type = 24;
|
||||||
|
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
heartbeatList,
|
||||||
|
uptimeList
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status page's manifest.json
|
||||||
|
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
response.json({
|
||||||
|
"name": statusPage.title,
|
||||||
|
"start_url": "/status/" + statusPage.slug,
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": statusPage.icon,
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
119
server/server.js
119
server/server.js
@@ -16,7 +16,7 @@ if (nodeVersion < requiredVersion) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const { sleep, log, getRandomInt, genSecret, debug, isDev } = require("../src/util");
|
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
log.info("server", "Welcome to Uptime Kuma");
|
log.info("server", "Welcome to Uptime Kuma");
|
||||||
@@ -35,6 +35,7 @@ const fs = require("fs");
|
|||||||
log.info("server", "Importing 3rd-party libraries");
|
log.info("server", "Importing 3rd-party libraries");
|
||||||
log.debug("server", "Importing express");
|
log.debug("server", "Importing express");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const expressStaticGzip = require("express-static-gzip");
|
||||||
log.debug("server", "Importing redbean-node");
|
log.debug("server", "Importing redbean-node");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
log.debug("server", "Importing jsonwebtoken");
|
log.debug("server", "Importing jsonwebtoken");
|
||||||
@@ -60,7 +61,7 @@ log.info("server", "Importing this project modules");
|
|||||||
log.debug("server", "Importing Monitor");
|
log.debug("server", "Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@@ -117,13 +118,14 @@ if (config.demoMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
const StatusPage = require("./model/status_page");
|
const StatusPage = require("./model/status_page");
|
||||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||||
|
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@@ -148,22 +150,6 @@ let jwtSecret = null;
|
|||||||
*/
|
*/
|
||||||
let needSetup = false;
|
let needSetup = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache Index HTML
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let indexHTML = "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|
||||||
} catch (e) {
|
|
||||||
// "dist/index.html" is not necessary for development
|
|
||||||
if (process.env.NODE_ENV !== "development") {
|
|
||||||
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase(testMode);
|
await initDatabase(testMode);
|
||||||
@@ -179,13 +165,25 @@ try {
|
|||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (request, response) => {
|
app.get("/", async (request, response) => {
|
||||||
debug(`Request Domain: ${request.hostname}`);
|
let hostname = request.hostname;
|
||||||
|
if (await setting("trustProxy")) {
|
||||||
|
const proxy = request.headers["x-forwarded-host"];
|
||||||
|
if (proxy) {
|
||||||
|
hostname = proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("entry", `Request Domain: ${hostname}`);
|
||||||
|
|
||||||
|
if (hostname in StatusPage.domainMappingList) {
|
||||||
|
log.debug("entry", "This is a status page domain");
|
||||||
|
|
||||||
|
let slug = StatusPage.domainMappingList[hostname];
|
||||||
|
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||||
|
|
||||||
if (request.hostname in StatusPage.domainMappingList) {
|
|
||||||
debug("This is a status page domain");
|
|
||||||
response.send(indexHTML);
|
|
||||||
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
}
|
}
|
||||||
@@ -214,7 +212,9 @@ try {
|
|||||||
// With Basic Auth using the first user's username/password
|
// With Basic Auth using the first user's username/password
|
||||||
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
||||||
|
|
||||||
app.use("/", express.static("dist"));
|
app.use("/", expressStaticGzip("dist", {
|
||||||
|
enableBrotli: true,
|
||||||
|
}));
|
||||||
|
|
||||||
// ./data/upload
|
// ./data/upload
|
||||||
app.use("/upload", express.static(Database.uploadDir));
|
app.use("/upload", express.static(Database.uploadDir));
|
||||||
@@ -227,12 +227,16 @@ try {
|
|||||||
const apiRouter = require("./routers/api-router");
|
const apiRouter = require("./routers/api-router");
|
||||||
app.use(apiRouter);
|
app.use(apiRouter);
|
||||||
|
|
||||||
|
// Status Page Router
|
||||||
|
const statusPageRouter = require("./routers/status-page-router");
|
||||||
|
app.use(statusPageRouter);
|
||||||
|
|
||||||
// Universal Route Handler, must be at the end of all express routes.
|
// Universal Route Handler, must be at the end of all express routes.
|
||||||
app.get("*", async (_request, response) => {
|
app.get("*", async (_request, response) => {
|
||||||
if (_request.originalUrl.startsWith("/upload/")) {
|
if (_request.originalUrl.startsWith("/upload/")) {
|
||||||
response.status(404).send("File not found.");
|
response.status(404).send("File not found.");
|
||||||
} else {
|
} else {
|
||||||
response.send(indexHTML);
|
response.send(server.indexHTML);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,7 +255,9 @@ try {
|
|||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
socket.on("loginByToken", async (token, callback) => {
|
socket.on("loginByToken", async (token, callback) => {
|
||||||
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
|
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let decoded = jwt.verify(token, jwtSecret);
|
let decoded = jwt.verify(token, jwtSecret);
|
||||||
@@ -267,14 +273,14 @@ try {
|
|||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
log.debug("auth", "afterLogin ok");
|
log.debug("auth", "afterLogin ok");
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -283,7 +289,7 @@ try {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Invalid token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -294,7 +300,9 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
|
log.info("auth", `Login by username + password. IP=${clientIP}`);
|
||||||
|
|
||||||
// Checking
|
// Checking
|
||||||
if (typeof callback !== "function") {
|
if (typeof callback !== "function") {
|
||||||
@@ -307,7 +315,7 @@ try {
|
|||||||
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
if (! await loginRateLimiter.pass(callback)) {
|
if (! await loginRateLimiter.pass(callback)) {
|
||||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +325,7 @@ try {
|
|||||||
if (user.twofa_status === 0) {
|
if (user.twofa_status === 0) {
|
||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -329,7 +337,7 @@ try {
|
|||||||
|
|
||||||
if (user.twofa_status === 1 && !data.token) {
|
if (user.twofa_status === 1 && !data.token) {
|
||||||
|
|
||||||
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
tokenRequired: true,
|
tokenRequired: true,
|
||||||
@@ -347,7 +355,7 @@ try {
|
|||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -357,7 +365,7 @@ try {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -367,7 +375,7 @@ try {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
|
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -439,6 +447,8 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("save2FA", async (currentPassword, callback) => {
|
socket.on("save2FA", async (currentPassword, callback) => {
|
||||||
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! await twoFaRateLimiter.pass(callback)) {
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
@@ -451,7 +461,7 @@ try {
|
|||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
|
log.info("auth", `Saved 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -459,7 +469,7 @@ try {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -469,6 +479,8 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disable2FA", async (currentPassword, callback) => {
|
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||||
|
const clientIP = await server.getClientIP(socket);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! await twoFaRateLimiter.pass(callback)) {
|
if (! await twoFaRateLimiter.pass(callback)) {
|
||||||
return;
|
return;
|
||||||
@@ -478,7 +490,7 @@ try {
|
|||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
await TwoFA.disable2FA(socket.userID);
|
await TwoFA.disable2FA(socket.userID);
|
||||||
|
|
||||||
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
|
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -486,7 +498,7 @@ try {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
|
log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -657,9 +669,10 @@ try {
|
|||||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
bean.interval = monitor.interval;
|
bean.interval = monitor.interval;
|
||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
|
bean.resendInterval = monitor.resendInterval;
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
bean.maxretries = monitor.maxretries;
|
bean.maxretries = monitor.maxretries;
|
||||||
bean.port = monitor.port;
|
bean.port = parseInt(monitor.port);
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
bean.ignoreTls = monitor.ignoreTls;
|
bean.ignoreTls = monitor.ignoreTls;
|
||||||
bean.expiryNotification = monitor.expiryNotification;
|
bean.expiryNotification = monitor.expiryNotification;
|
||||||
@@ -669,11 +682,23 @@ try {
|
|||||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||||
bean.pushToken = monitor.pushToken;
|
bean.pushToken = monitor.pushToken;
|
||||||
|
bean.docker_container = monitor.docker_container;
|
||||||
|
bean.docker_host = monitor.docker_host;
|
||||||
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||||
bean.mqttUsername = monitor.mqttUsername;
|
bean.mqttUsername = monitor.mqttUsername;
|
||||||
bean.mqttPassword = monitor.mqttPassword;
|
bean.mqttPassword = monitor.mqttPassword;
|
||||||
bean.mqttTopic = monitor.mqttTopic;
|
bean.mqttTopic = monitor.mqttTopic;
|
||||||
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
||||||
|
bean.databaseConnectionString = monitor.databaseConnectionString;
|
||||||
|
bean.databaseQuery = monitor.databaseQuery;
|
||||||
|
bean.authMethod = monitor.authMethod;
|
||||||
|
bean.authWorkstation = monitor.authWorkstation;
|
||||||
|
bean.authDomain = monitor.authDomain;
|
||||||
|
bean.radiusUsername = monitor.radiusUsername;
|
||||||
|
bean.radiusPassword = monitor.radiusPassword;
|
||||||
|
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||||
|
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||||
|
bean.radiusSecret = monitor.radiusSecret;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
@@ -1247,10 +1272,14 @@ try {
|
|||||||
method: monitorListData[i].method || "GET",
|
method: monitorListData[i].method || "GET",
|
||||||
body: monitorListData[i].body,
|
body: monitorListData[i].body,
|
||||||
headers: monitorListData[i].headers,
|
headers: monitorListData[i].headers,
|
||||||
|
authMethod: monitorListData[i].authMethod,
|
||||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||||
|
authWorkstation: monitorListData[i].authWorkstation,
|
||||||
|
authDomain: monitorListData[i].authDomain,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
|
resendInterval: monitorListData[i].resendInterval || 0,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
maxretries: monitorListData[i].maxretries,
|
maxretries: monitorListData[i].maxretries,
|
||||||
port: monitorListData[i].port,
|
port: monitorListData[i].port,
|
||||||
@@ -1419,6 +1448,7 @@ try {
|
|||||||
cloudflaredSocketHandler(socket);
|
cloudflaredSocketHandler(socket);
|
||||||
databaseSocketHandler(socket);
|
databaseSocketHandler(socket);
|
||||||
proxySocketHandler(socket);
|
proxySocketHandler(socket);
|
||||||
|
dockerSocketHandler(socket);
|
||||||
|
|
||||||
log.debug("server", "added all socket handlers");
|
log.debug("server", "added all socket handlers");
|
||||||
|
|
||||||
@@ -1519,6 +1549,7 @@ async function afterLogin(socket, user) {
|
|||||||
let monitorList = await server.sendMonitorList(socket);
|
let monitorList = await server.sendMonitorList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
|
sendDockerHostList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@@ -1673,10 +1704,6 @@ async function shutdownFunction(signal) {
|
|||||||
await cloudflaredStop();
|
await cloudflaredStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientIp(socket) {
|
|
||||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Final function called before application exits */
|
/** Final function called before application exits */
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
log.info("server", "Graceful shutdown successful!");
|
log.info("server", "Graceful shutdown successful!");
|
||||||
@@ -1694,6 +1721,6 @@ gracefulShutdown(server.httpServer, {
|
|||||||
// Catch unexpected errors here
|
// Catch unexpected errors here
|
||||||
process.addListener("unhandledRejection", (error, promise) => {
|
process.addListener("unhandledRejection", (error, promise) => {
|
||||||
console.trace(error);
|
console.trace(error);
|
||||||
errorLog(error, false);
|
UptimeKumaServer.errorLog(error, false);
|
||||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
});
|
});
|
||||||
|
165
server/settings.js
Normal file
165
server/settings.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* key1: {
|
||||||
|
* value: "value2",
|
||||||
|
* timestamp: 12345678
|
||||||
|
* },
|
||||||
|
* key2: {
|
||||||
|
* value: 2,
|
||||||
|
* timestamp: 12345678
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
static cacheList = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
static cacheCleaner = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve value of setting based on key
|
||||||
|
* @param {string} key Key of setting to retrieve
|
||||||
|
* @returns {Promise<any>} Value
|
||||||
|
*/
|
||||||
|
static async get(key) {
|
||||||
|
|
||||||
|
// Start cache clear if not started yet
|
||||||
|
if (!Settings.cacheCleaner) {
|
||||||
|
Settings.cacheCleaner = setInterval(() => {
|
||||||
|
log.debug("settings", "Cache Cleaner is just started.");
|
||||||
|
for (key in Settings.cacheList) {
|
||||||
|
if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) {
|
||||||
|
log.debug("settings", "Cache Cleaner deleted: " + key);
|
||||||
|
delete Settings.cacheList[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query from cache
|
||||||
|
if (key in Settings.cacheList) {
|
||||||
|
const v = Settings.cacheList[key].value;
|
||||||
|
log.debug("settings", `Get Setting (cache): ${key}: ${v}`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
|
key,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(value);
|
||||||
|
log.debug("settings", `Get Setting: ${key}: ${v}`);
|
||||||
|
|
||||||
|
Settings.cacheList[key] = {
|
||||||
|
value: v,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return v;
|
||||||
|
} catch (e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified setting to specified value
|
||||||
|
* @param {string} key Key of setting to set
|
||||||
|
* @param {any} value Value to set to
|
||||||
|
* @param {?string} type Type of setting
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async set(key, value, type = null) {
|
||||||
|
|
||||||
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
|
key,
|
||||||
|
]);
|
||||||
|
if (!bean) {
|
||||||
|
bean = R.dispense("setting");
|
||||||
|
bean.key = key;
|
||||||
|
}
|
||||||
|
bean.type = type;
|
||||||
|
bean.value = JSON.stringify(value);
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
Settings.deleteCache([ key ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings based on type
|
||||||
|
* @param {string} type The type of setting
|
||||||
|
* @returns {Promise<Bean>}
|
||||||
|
*/
|
||||||
|
static async getSettings(type) {
|
||||||
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let row of list) {
|
||||||
|
try {
|
||||||
|
result[row.key] = JSON.parse(row.value);
|
||||||
|
} catch (e) {
|
||||||
|
result[row.key] = row.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set settings based on type
|
||||||
|
* @param {string} type Type of settings to set
|
||||||
|
* @param {Object} data Values of settings
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async setSettings(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);
|
||||||
|
|
||||||
|
Settings.deleteCache(keyList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} keyList
|
||||||
|
*/
|
||||||
|
static deleteCache(keyList) {
|
||||||
|
for (let key of keyList) {
|
||||||
|
delete Settings.cacheList[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Settings,
|
||||||
|
};
|
@@ -63,7 +63,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
const disabledAuth = await setting("disableAuth");
|
||||||
|
if (!disabledAuth) {
|
||||||
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
|
}
|
||||||
cloudflared.stop();
|
cloudflared.stop();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
|
79
server/socket-handlers/docker-socket-handler.js
Normal file
79
server/socket-handlers/docker-socket-handler.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const { sendDockerHostList } = require("../client");
|
||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { DockerHost } = require("../docker");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for docker hosts
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
|
module.exports.dockerSocketHandler = (socket) => {
|
||||||
|
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved",
|
||||||
|
id: dockerHostBean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await DockerHost.delete(dockerHostID, socket.userID);
|
||||||
|
await sendDockerHostList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testDockerHost", async (dockerHost, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let amount = await DockerHost.testDockerHost(dockerHost);
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (amount > 1) {
|
||||||
|
msg = "Connected Successfully. Amount of containers: " + amount;
|
||||||
|
} else {
|
||||||
|
msg = "Connected Successfully, but there are no containers?";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error("docker", e);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -202,6 +202,11 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
relationBean.weight = monitorOrder++;
|
relationBean.weight = monitorOrder++;
|
||||||
relationBean.group_id = groupBean.id;
|
relationBean.group_id = groupBean.id;
|
||||||
relationBean.monitor_id = monitor.id;
|
relationBean.monitor_id = monitor.id;
|
||||||
|
|
||||||
|
if (monitor.sendUrl !== undefined) {
|
||||||
|
relationBean.send_url = monitor.sendUrl;
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(relationBean);
|
await R.store(relationBean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,13 +5,16 @@ const http = require("http");
|
|||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
const Database = require("./database");
|
||||||
|
const util = require("util");
|
||||||
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||||
* @type {UptimeKumaServer}
|
* @type {UptimeKumaServer}
|
||||||
*/
|
*/
|
||||||
class UptimeKumaServer {
|
class UptimeKumaServer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {UptimeKumaServer}
|
* @type {UptimeKumaServer}
|
||||||
@@ -28,6 +31,12 @@ class UptimeKumaServer {
|
|||||||
httpServer = undefined;
|
httpServer = undefined;
|
||||||
io = undefined;
|
io = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache Index HTML
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
indexHTML = "";
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
@@ -42,7 +51,6 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
log.info("server", "Creating express and socket.io instance");
|
log.info("server", "Creating express and socket.io instance");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|
||||||
if (sslKey && sslCert) {
|
if (sslKey && sslCert) {
|
||||||
log.info("server", "Server Type: HTTPS");
|
log.info("server", "Server Type: HTTPS");
|
||||||
this.httpServer = https.createServer({
|
this.httpServer = https.createServer({
|
||||||
@@ -54,6 +62,18 @@ class UptimeKumaServer {
|
|||||||
this.httpServer = http.createServer(this.app);
|
this.httpServer = http.createServer(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
|
} catch (e) {
|
||||||
|
// "dist/index.html" is not necessary for development
|
||||||
|
if (process.env.NODE_ENV !== "development") {
|
||||||
|
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheableDnsHttpAgent.registerGlobalAgent();
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +103,48 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write error to log file
|
||||||
|
* @param {any} error The error to write
|
||||||
|
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||||
|
*/
|
||||||
|
static errorLog(error, outputToConsole = true) {
|
||||||
|
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||||
|
flags: "a"
|
||||||
|
});
|
||||||
|
|
||||||
|
errorLogStream.on("error", () => {
|
||||||
|
log.info("", "Cannot write to error.log");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorLogStream) {
|
||||||
|
const dateTime = R.isoDateTime();
|
||||||
|
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
|
||||||
|
|
||||||
|
if (outputToConsole) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorLogStream.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getClientIP(socket) {
|
||||||
|
let clientIP = socket.client.conn.remoteAddress;
|
||||||
|
|
||||||
|
if (clientIP === undefined) {
|
||||||
|
clientIP = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await Settings.get("trustProxy")) {
|
||||||
|
return socket.client.conn.request.headers["x-forwarded-for"]
|
||||||
|
|| socket.client.conn.request.headers["x-real-ip"]
|
||||||
|
|| clientIP.replace(/^.*:/, "");
|
||||||
|
} else {
|
||||||
|
return clientIP.replace(/^.*:/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -7,9 +7,20 @@ const { Resolver } = require("dns");
|
|||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
const chardet = require("chardet");
|
const chardet = require("chardet");
|
||||||
const fs = require("fs");
|
|
||||||
const nodeJsUtil = require("util");
|
|
||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
|
const chroma = require("chroma-js");
|
||||||
|
const { badgeConstants } = require("./config");
|
||||||
|
const mssql = require("mssql");
|
||||||
|
const { Client } = require("pg");
|
||||||
|
const postgresConParse = require("pg-connection-string").parse;
|
||||||
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
const radiusClient = require("node-radius-client");
|
||||||
|
const {
|
||||||
|
dictionaries: {
|
||||||
|
rfc2865: { file, attributes },
|
||||||
|
},
|
||||||
|
} = require("node-radius-utils");
|
||||||
|
|
||||||
// From ping-lite
|
// From ping-lite
|
||||||
exports.WIN = /^win/.test(process.platform);
|
exports.WIN = /^win/.test(process.platform);
|
||||||
@@ -172,16 +183,40 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use NTLM Auth for a http request.
|
||||||
|
* @param {Object} options The http request options
|
||||||
|
* @param {Object} ntlmOptions The auth options
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.httpNtlm = function (options, ntlmOptions) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let client = NtlmClient(ntlmOptions);
|
||||||
|
|
||||||
|
client(options)
|
||||||
|
.then((resp) => {
|
||||||
|
resolve(resp);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a given record using the specified DNS server
|
* Resolves a given record using the specified DNS server
|
||||||
* @param {string} hostname The hostname of the record to lookup
|
* @param {string} hostname The hostname of the record to lookup
|
||||||
* @param {string} resolverServer The DNS server to use
|
* @param {string} resolverServer The DNS server to use
|
||||||
|
* @param {string} resolverPort Port the DNS server is listening on
|
||||||
* @param {string} rrtype The type of record to request
|
* @param {string} rrtype The type of record to request
|
||||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
*/
|
*/
|
||||||
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
resolver.setServers([ resolverServer ]);
|
// Remove brackets from IPv6 addresses so we can re-add them to
|
||||||
|
// prevent issues with ::1:5300 (::1 port 5300)
|
||||||
|
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||||
|
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (rrtype === "PTR") {
|
if (rrtype === "PTR") {
|
||||||
resolver.reverse(hostname, (err, records) => {
|
resolver.reverse(hostname, (err, records) => {
|
||||||
@@ -203,23 +238,91 @@ exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a query on SQL Server
|
||||||
|
* @param {string} connectionString The database connection string
|
||||||
|
* @param {string} query The query to validate the database with
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.mssqlQuery = function (connectionString, query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
mssql.connect(connectionString).then(pool => {
|
||||||
|
return pool.request()
|
||||||
|
.query(query);
|
||||||
|
}).then(result => {
|
||||||
|
resolve(result);
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
}).finally(() => {
|
||||||
|
mssql.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a query on Postgres
|
||||||
|
* @param {string} connectionString The database connection string
|
||||||
|
* @param {string} query The query to validate the database with
|
||||||
|
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||||
|
*/
|
||||||
|
exports.postgresQuery = function (connectionString, query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const config = postgresConParse(connectionString);
|
||||||
|
|
||||||
|
if (config.password === "") {
|
||||||
|
// See https://github.com/brianc/node-postgres/issues/1927
|
||||||
|
return reject(new Error("Password is undefined."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({ connectionString });
|
||||||
|
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
return client.query(query)
|
||||||
|
.then(res => {
|
||||||
|
resolve(res);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.radius = function (
|
||||||
|
hostname,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
calledStationId,
|
||||||
|
callingStationId,
|
||||||
|
secret,
|
||||||
|
) {
|
||||||
|
const client = new radiusClient({
|
||||||
|
host: hostname,
|
||||||
|
dictionaries: [ file ],
|
||||||
|
});
|
||||||
|
|
||||||
|
return client.accessRequest({
|
||||||
|
secret: secret,
|
||||||
|
attributes: [
|
||||||
|
[ attributes.USER_NAME, username ],
|
||||||
|
[ attributes.USER_PASSWORD, password ],
|
||||||
|
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||||
|
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve value of setting based on key
|
* Retrieve value of setting based on key
|
||||||
* @param {string} key Key of setting to retrieve
|
* @param {string} key Key of setting to retrieve
|
||||||
* @returns {Promise<Object>} Object representation of setting
|
* @returns {Promise<any>} Value
|
||||||
|
* @deprecated Use await Settings.get(key)
|
||||||
*/
|
*/
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
return await Settings.get(key);
|
||||||
key,
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const v = JSON.parse(value);
|
|
||||||
log.debug("util", `Get Setting: ${key}: ${v}`);
|
|
||||||
return v;
|
|
||||||
} catch (e) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,70 +333,26 @@ exports.setting = async function (key) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setSetting = async function (key, value, type = null) {
|
exports.setSetting = async function (key, value, type = null) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
await Settings.set(key, value, type);
|
||||||
key,
|
|
||||||
]);
|
|
||||||
if (!bean) {
|
|
||||||
bean = R.dispense("setting");
|
|
||||||
bean.key = key;
|
|
||||||
}
|
|
||||||
bean.type = type;
|
|
||||||
bean.value = JSON.stringify(value);
|
|
||||||
await R.store(bean);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get settings based on type
|
* Get settings based on type
|
||||||
* @param {?string} type The type of setting
|
* @param {string} type The type of setting
|
||||||
* @returns {Promise<Bean>}
|
* @returns {Promise<Bean>}
|
||||||
*/
|
*/
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
return await Settings.getSettings(type);
|
||||||
type,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let result = {};
|
|
||||||
|
|
||||||
for (let row of list) {
|
|
||||||
try {
|
|
||||||
result[row.key] = JSON.parse(row.value);
|
|
||||||
} catch (e) {
|
|
||||||
result[row.key] = row.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set settings based on type
|
* Set settings based on type
|
||||||
* @param {?string} type Type of settings to set
|
* @param {string} type Type of settings to set
|
||||||
* @param {Object} data Values of settings
|
* @param {Object} data Values of settings
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
await Settings.setSettings(type, 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
|
// ssl-checker by @dyaa
|
||||||
@@ -386,7 +445,7 @@ exports.checkCertificate = function (res) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the provided status code is within the accepted ranges
|
* Check if the provided status code is within the accepted ranges
|
||||||
* @param {string} status The status code to check
|
* @param {number} status The status code to check
|
||||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||||
* @returns {boolean} True if status code within range, false otherwise
|
* @returns {boolean} True if status code within range, false otherwise
|
||||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||||
@@ -525,28 +584,44 @@ exports.convertToUTF8 = (body) => {
|
|||||||
return str.toString();
|
return str.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
let logFile;
|
/**
|
||||||
|
* Returns a color code in hex format based on a given percentage:
|
||||||
try {
|
* 0% => hue = 10 => red
|
||||||
logFile = fs.createWriteStream("./data/error.log", {
|
* 100% => hue = 90 => green
|
||||||
flags: "a"
|
*
|
||||||
});
|
* @param {number} percentage float, 0 to 1
|
||||||
} catch (_) { }
|
* @param {number} maxHue
|
||||||
|
* @param {number} minHue, int
|
||||||
|
* @returns {string}, hex value
|
||||||
|
*/
|
||||||
|
exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
|
||||||
|
const hue = percentage * (maxHue - minHue) + minHue;
|
||||||
|
try {
|
||||||
|
return chroma(`hsl(${hue}, 90%, 40%)`).hex();
|
||||||
|
} catch (err) {
|
||||||
|
return badgeConstants.naColor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write error to log file
|
* Joins and array of string to one string after filtering out empty values
|
||||||
* @param {any} error The error to write
|
*
|
||||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
* @param {string[]} parts
|
||||||
|
* @param {string} connector
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
exports.errorLog = (error, outputToConsole = true) => {
|
exports.filterAndJoin = (parts, connector = "") => {
|
||||||
try {
|
return parts.filter((part) => !!part && part !== "").join(connector);
|
||||||
if (logFile) {
|
};
|
||||||
const dateTime = R.isoDateTime();
|
|
||||||
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
/**
|
||||||
|
* Send a 403 response
|
||||||
if (outputToConsole) {
|
* @param {Object} res Express response object
|
||||||
console.error(error);
|
* @param {string} [msg=""] Message to send
|
||||||
}
|
*/
|
||||||
}
|
module.exports.send403 = (res, msg = "") => {
|
||||||
} catch (_) { }
|
res.status(403).json({
|
||||||
|
"status": "fail",
|
||||||
|
"msg": msg,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -34,6 +34,25 @@ textarea.form-control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// optgroup
|
||||||
|
optgroup {
|
||||||
|
color: #b1b1b1;
|
||||||
|
option {
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
optgroup {
|
||||||
|
color: #535864;
|
||||||
|
option {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollbar
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -363,6 +382,12 @@ textarea.form-control {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100% - 65px);
|
height: calc(100% - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 770px) {
|
||||||
|
&.scrollbar {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -473,6 +498,14 @@ textarea.form-control {
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5.settings-subheading::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-bottom: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
86
src/components/ActionInput.vue
Normal file
86
src/components/ActionInput.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="model"
|
||||||
|
class="form-control"
|
||||||
|
:type="type"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="!enabled"
|
||||||
|
>
|
||||||
|
<a class="btn btn-outline-primary" @click="action()">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Generic input field with a customizable action on the right.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* The value of the input field.
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the input field is enabled / disabled.
|
||||||
|
*/
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Placeholder text for the input field.
|
||||||
|
*/
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The icon displayed in the right button of the input field.
|
||||||
|
* Accepts a Font Awesome icon string identifier.
|
||||||
|
* @example "plus"
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The input type of the input field.
|
||||||
|
* @example "email"
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text",
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The action to be performed when the button is clicked.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Send value update to parent on change.
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -25,10 +25,12 @@ export default {
|
|||||||
CertificateInfoRow,
|
CertificateInfoRow,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
certInfo: {
|
certInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Is the TLS certificate valid? */
|
||||||
valid: {
|
valid: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
@@ -56,12 +56,19 @@ export default {
|
|||||||
Datetime,
|
Datetime,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing certificate */
|
||||||
cert: {
|
cert: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Format the subject of the certificate
|
||||||
|
* @param {Object} subject Object representing the certificates
|
||||||
|
* subject
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
formatSubject(subject) {
|
formatSubject(subject) {
|
||||||
if (subject.O && subject.CN && subject.C) {
|
if (subject.O && subject.CN && subject.C) {
|
||||||
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||||
|
@@ -29,14 +29,17 @@ import { Modal } from "bootstrap";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Style of button */
|
||||||
btnStyle: {
|
btnStyle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "btn-primary",
|
default: "btn-primary",
|
||||||
},
|
},
|
||||||
|
/** Text to use as yes */
|
||||||
yesText: {
|
yesText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Yes", // TODO: No idea what to translate this
|
default: "Yes", // TODO: No idea what to translate this
|
||||||
},
|
},
|
||||||
|
/** Text to use as no */
|
||||||
noText: {
|
noText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "No",
|
default: "No",
|
||||||
@@ -50,9 +53,13 @@ export default {
|
|||||||
this.modal = new Modal(this.$refs.modal);
|
this.modal = new Modal(this.$refs.modal);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the confirm dialog */
|
||||||
show() {
|
show() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @emits string "yes" Notify the parent when Yes is pressed
|
||||||
|
*/
|
||||||
yes() {
|
yes() {
|
||||||
this.$emit("yes");
|
this.$emit("yes");
|
||||||
},
|
},
|
||||||
|
@@ -25,33 +25,41 @@ let timeout;
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** ID of this input */
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Type of input */
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "text"
|
default: "text"
|
||||||
},
|
},
|
||||||
|
/** The value of the input */
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
},
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
readonly: {
|
readonly: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input disabled? */
|
||||||
disabled: {
|
disabled: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -79,14 +87,21 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/** Show the input */
|
||||||
showInput() {
|
showInput() {
|
||||||
this.visibility = "text";
|
this.visibility = "text";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Hide the input */
|
||||||
hideInput() {
|
hideInput() {
|
||||||
this.visibility = "password";
|
this.visibility = "password";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the provided text to the users clipboard
|
||||||
|
* @param {string} textToCopy
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
copyToClipboard(textToCopy) {
|
copyToClipboard(textToCopy) {
|
||||||
this.icon = "check";
|
this.icon = "check";
|
||||||
|
|
||||||
|
@@ -10,11 +10,16 @@ import { sleep } from "../util.ts";
|
|||||||
export default {
|
export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: [ String, Number ],
|
/** Value to count */
|
||||||
|
value: {
|
||||||
|
type: [ String, Number ],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
time: {
|
time: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
},
|
},
|
||||||
|
/** Unit of the value */
|
||||||
unit: {
|
unit: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "ms",
|
default: "ms",
|
||||||
@@ -40,9 +45,7 @@ export default {
|
|||||||
let frames = 12;
|
let frames = 12;
|
||||||
let step = Math.floor(diff / frames);
|
let step = Math.floor(diff / frames);
|
||||||
|
|
||||||
if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) {
|
if (! (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0)) {
|
||||||
// Lazy to NOT this condition, hahaha.
|
|
||||||
} else {
|
|
||||||
for (let i = 1; i < frames; i++) {
|
for (let i = 1; i < frames; i++) {
|
||||||
this.output += step;
|
this.output += step;
|
||||||
await sleep(15);
|
await sleep(15);
|
||||||
|
@@ -13,7 +13,12 @@ dayjs.extend(relativeTime);
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: String,
|
/** Value of date time */
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Should only the date be displayed? */
|
||||||
dateOnly: {
|
dateOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
177
src/components/DockerHostDialog.vue
Normal file
177
src/components/DockerHostDialog.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</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="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||||
|
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
|
||||||
|
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
|
||||||
|
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
|
||||||
|
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("Examples") }}:
|
||||||
|
<ul>
|
||||||
|
<li>/var/run/docker.sock</li>
|
||||||
|
<li>tcp://localhost:2375</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<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">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
|
||||||
|
{{ $t("deleteDockerHostMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
processing: false,
|
||||||
|
id: null,
|
||||||
|
connectionTypes: [ "socket", "tcp" ],
|
||||||
|
dockerHost: {
|
||||||
|
name: "",
|
||||||
|
dockerDaemon: "",
|
||||||
|
dockerType: "",
|
||||||
|
// Do not set default value here, please scroll to show()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
deleteConfirm() {
|
||||||
|
this.modal.hide();
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
show(dockerHostID) {
|
||||||
|
if (dockerHostID) {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
this.id = dockerHostID;
|
||||||
|
|
||||||
|
for (let n of this.$root.dockerHostList) {
|
||||||
|
if (n.id === dockerHostID) {
|
||||||
|
this.dockerHost = n;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
toast.error("Docker Host not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.id = null;
|
||||||
|
this.dockerHost = {
|
||||||
|
name: "",
|
||||||
|
dockerType: "socket",
|
||||||
|
dockerDaemon: "/var/run/docker.sock",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
|
||||||
|
// Emit added event, doesn't emit edit.
|
||||||
|
if (! this.id) {
|
||||||
|
this.$emit("added", res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
test() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDockerHost() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -17,14 +17,17 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Size of the heartbeat bar */
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "big",
|
default: "big",
|
||||||
},
|
},
|
||||||
|
/** ID of the monitor */
|
||||||
monitorId: {
|
monitorId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Array of the monitors heartbeats */
|
||||||
heartbeatList: {
|
heartbeatList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: null,
|
default: null,
|
||||||
@@ -160,15 +163,23 @@ export default {
|
|||||||
this.resize();
|
this.resize();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Resize the heartbeat bar */
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title of the beat.
|
||||||
|
* Used as the hover tooltip on the heartbeat bar.
|
||||||
|
* @param {Object} beat Beat to get title from
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getBeatTitle(beat) {
|
getBeatTitle(beat) {
|
||||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@@ -24,25 +24,31 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** The value of the input */
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
/** Maximum length of the input */
|
||||||
maxlength: {
|
maxlength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 255
|
default: 255
|
||||||
},
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
/** Is the input required? */
|
||||||
required: {
|
required: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
},
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
readonly: {
|
readonly: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -68,9 +74,11 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show users input in plain text */
|
||||||
showInput() {
|
showInput() {
|
||||||
this.visibility = "text";
|
this.visibility = "text";
|
||||||
},
|
},
|
||||||
|
/** Censor users input */
|
||||||
hideInput() {
|
hideInput() {
|
||||||
this.visibility = "password";
|
this.visibility = "password";
|
||||||
},
|
},
|
||||||
|
@@ -55,6 +55,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Submit the user details and attempt to log in */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
@@ -58,6 +58,7 @@ export default {
|
|||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Should the scrollbar be shown */
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
@@ -69,10 +70,22 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* Improve the sticky appearance of the list by increasing its
|
||||||
|
* height as user scrolls down.
|
||||||
|
* Not used on mobile.
|
||||||
|
*/
|
||||||
boxStyle() {
|
boxStyle() {
|
||||||
return {
|
if (window.innerWidth > 550) {
|
||||||
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
return {
|
||||||
};
|
height: `calc(100vh - 160px + ${this.windowTop}px)`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
height: "calc(100vh - 160px)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
@@ -124,6 +137,7 @@ export default {
|
|||||||
window.removeEventListener("scroll", this.onScroll);
|
window.removeEventListener("scroll", this.onScroll);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Handle user scroll */
|
||||||
onScroll() {
|
onScroll() {
|
||||||
if (window.top.scrollY <= 133) {
|
if (window.top.scrollY <= 133) {
|
||||||
this.windowTop = window.top.scrollY;
|
this.windowTop = window.top.scrollY;
|
||||||
@@ -131,9 +145,15 @@ export default {
|
|||||||
this.windowTop = 133;
|
this.windowTop = 133;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get URL of monitor
|
||||||
|
* @param {number} id ID of monitor
|
||||||
|
* @returns {string} Relative URL of monitor
|
||||||
|
*/
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
|
/** Clear the search bar */
|
||||||
clearSearchText() {
|
clearSearchText() {
|
||||||
this.searchText = "";
|
this.searchText = "";
|
||||||
}
|
}
|
||||||
|
@@ -125,11 +125,16 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
/** Show dialog to confirm deletion */
|
||||||
deleteConfirm() {
|
deleteConfirm() {
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
this.$refs.confirmDelete.show();
|
this.$refs.confirmDelete.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show settings for specified notification
|
||||||
|
* @param {number} notificationID ID of notification to show
|
||||||
|
*/
|
||||||
show(notificationID) {
|
show(notificationID) {
|
||||||
if (notificationID) {
|
if (notificationID) {
|
||||||
this.id = notificationID;
|
this.id = notificationID;
|
||||||
@@ -152,6 +157,7 @@ export default {
|
|||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Submit the form to the server */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||||
@@ -170,6 +176,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Test the notification endpoint */
|
||||||
test() {
|
test() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||||
@@ -178,6 +185,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete the notification endpoint */
|
||||||
deleteNotification() {
|
deleteNotification() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||||
@@ -190,6 +198,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
* Get a unique default name for the notification
|
||||||
* @param {keyof NotificationFormList} notificationKey
|
* @param {keyof NotificationFormList} notificationKey
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
@@ -24,7 +24,7 @@ import timezone from "dayjs/plugin/timezone";
|
|||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { LineChart } from "vue-chart-3";
|
import { LineChart } from "vue-chart-3";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import { DOWN } from "../util.ts";
|
import { DOWN, log } from "../util.ts";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
@@ -35,6 +35,7 @@ Chart.register(LineController, BarController, LineElement, PointElement, TimeSca
|
|||||||
export default {
|
export default {
|
||||||
components: { LineChart },
|
components: { LineChart },
|
||||||
props: {
|
props: {
|
||||||
|
/** ID of monitor */
|
||||||
monitorId: {
|
monitorId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -217,8 +218,9 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
// Update chart data when the selected chart period changes
|
// Update chart data when the selected chart period changes
|
||||||
chartPeriodHrs: function (newPeriod) {
|
chartPeriodHrs: function (newPeriod) {
|
||||||
if (newPeriod === "0") {
|
|
||||||
newPeriod = null;
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (newPeriod == "0") {
|
||||||
this.heartbeatList = null;
|
this.heartbeatList = null;
|
||||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -241,7 +243,11 @@ export default {
|
|||||||
// And mirror latest change to this.heartbeatList
|
// And mirror latest change to this.heartbeatList
|
||||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||||
(heartbeatList) => {
|
(heartbeatList) => {
|
||||||
if (this.chartPeriodHrs !== 0) {
|
|
||||||
|
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (this.chartPeriodHrs != "0") {
|
||||||
const newBeat = heartbeatList.at(-1);
|
const newBeat = heartbeatList.at(-1);
|
||||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||||
this.heartbeatList.push(heartbeatList.at(-1));
|
this.heartbeatList.push(heartbeatList.at(-1));
|
||||||
|
@@ -130,11 +130,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show dialog to confirm deletion */
|
||||||
deleteConfirm() {
|
deleteConfirm() {
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
this.$refs.confirmDelete.show();
|
this.$refs.confirmDelete.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show settings for specified proxy
|
||||||
|
* @param {number} proxyID ID of proxy to show
|
||||||
|
*/
|
||||||
show(proxyID) {
|
show(proxyID) {
|
||||||
if (proxyID) {
|
if (proxyID) {
|
||||||
this.id = proxyID;
|
this.id = proxyID;
|
||||||
@@ -163,6 +168,7 @@ export default {
|
|||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Submit form data for saving */
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
||||||
@@ -180,6 +186,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete this proxy */
|
||||||
deleteProxy() {
|
deleteProxy() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
||||||
|
@@ -39,7 +39,27 @@
|
|||||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
|
|
||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
{{ monitor.element.name }}
|
<a
|
||||||
|
v-if="showLink(monitor)"
|
||||||
|
:href="monitor.element.url"
|
||||||
|
class="item-name"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ monitor.element.name }}
|
||||||
|
</a>
|
||||||
|
<p v-else class="item-name"> {{ monitor.element.name }} </p>
|
||||||
|
<span
|
||||||
|
v-if="showLink(monitor, true)"
|
||||||
|
title="Toggle Clickable Link"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="editMode"
|
||||||
|
:class="{'link-active': monitor.element.sendUrl, 'btn-link': true}"
|
||||||
|
icon="link" class="action me-3"
|
||||||
|
|
||||||
|
@click="toggleLink(group.index, monitor.index)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showTags" class="tags">
|
<div v-if="showTags" class="tags">
|
||||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
@@ -72,10 +92,12 @@ export default {
|
|||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Are we in edit mode? */
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Should tags be shown? */
|
||||||
showTags: {
|
showTags: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
}
|
}
|
||||||
@@ -94,13 +116,50 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Remove the specified group
|
||||||
|
* @param {number} index Index of group to remove
|
||||||
|
*/
|
||||||
removeGroup(index) {
|
removeGroup(index) {
|
||||||
this.$root.publicGroupList.splice(index, 1);
|
this.$root.publicGroupList.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a monitor from a group
|
||||||
|
* @param {number} groupIndex Index of group to remove monitor
|
||||||
|
* from
|
||||||
|
* @param {number} index Index of monitor to remove
|
||||||
|
*/
|
||||||
removeMonitor(groupIndex, index) {
|
removeMonitor(groupIndex, index) {
|
||||||
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the value of sendUrl
|
||||||
|
* @param {number} groupIndex Index of group monitor is member of
|
||||||
|
* @param {number} index Index of monitor within group
|
||||||
|
*/
|
||||||
|
toggleLink(groupIndex, index) {
|
||||||
|
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should a link to the monitor be shown?
|
||||||
|
* Attempts to guess if a link should be shown based upon if
|
||||||
|
* sendUrl is set and if the URL is default or not.
|
||||||
|
* @param {Object} monitor Monitor to check
|
||||||
|
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
|
||||||
|
* property be ignored. This will only work in edit mode.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
showLink(monitor, ignoreSendUrl = false) {
|
||||||
|
// We must check if there are any elements in monitorList to
|
||||||
|
// prevent undefined errors if it hasn't been loaded yet
|
||||||
|
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||||
|
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
||||||
|
}
|
||||||
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -119,6 +178,22 @@ export default {
|
|||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
color: #bbbbbb;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-active {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
.flip-list-move {
|
.flip-list-move {
|
||||||
transition: transform 0.5s;
|
transition: transform 0.5s;
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
status: Number,
|
/** Current status of monitor */
|
||||||
|
status: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@@ -20,14 +20,20 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Object representing tag */
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** Function to remove tag */
|
||||||
remove: {
|
remove: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Size of tag
|
||||||
|
* @values normal, small
|
||||||
|
*/
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "normal",
|
default: "normal",
|
||||||
|
@@ -139,6 +139,7 @@ export default {
|
|||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** Array of tags to be pre-selected */
|
||||||
preSelectedTags: {
|
preSelectedTags: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -241,9 +242,11 @@ export default {
|
|||||||
this.getExistingTags();
|
this.getExistingTags();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the add tag dialog */
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
/** Get all existing tags */
|
||||||
getExistingTags() {
|
getExistingTags() {
|
||||||
this.$root.getSocket().emit("getTags", (res) => {
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -253,6 +256,10 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Delete the specified tag
|
||||||
|
* @param {Object} tag Object representing tag to delete
|
||||||
|
*/
|
||||||
deleteTag(item) {
|
deleteTag(item) {
|
||||||
if (item.new) {
|
if (item.new) {
|
||||||
// Undo Adding a new Tag
|
// Undo Adding a new Tag
|
||||||
@@ -262,6 +269,13 @@ export default {
|
|||||||
this.deleteTags.push(item);
|
this.deleteTags.push(item);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get colour of text inside the tag
|
||||||
|
* @param {Object} option The tag that needs to be displayed.
|
||||||
|
* Defaults to "white" unless the tag has no color, which will
|
||||||
|
* then return the body color (based on application theme)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
textColor(option) {
|
textColor(option) {
|
||||||
if (option.color) {
|
if (option.color) {
|
||||||
return "white";
|
return "white";
|
||||||
@@ -269,6 +283,7 @@ export default {
|
|||||||
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** Add a draft tag */
|
||||||
addDraftTag() {
|
addDraftTag() {
|
||||||
console.log("Adding Draft Tag: ", this.newDraftTag);
|
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||||
if (this.newDraftTag.select != null) {
|
if (this.newDraftTag.select != null) {
|
||||||
@@ -296,6 +311,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.clearDraftTag();
|
this.clearDraftTag();
|
||||||
},
|
},
|
||||||
|
/** Remove a draft tag */
|
||||||
clearDraftTag() {
|
clearDraftTag() {
|
||||||
this.newDraftTag = {
|
this.newDraftTag = {
|
||||||
name: null,
|
name: null,
|
||||||
@@ -307,26 +323,51 @@ export default {
|
|||||||
};
|
};
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add a tag asynchronously
|
||||||
|
* @param {Object} newTag Object representing new tag to add
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
addTagAsync(newTag) {
|
addTagAsync(newTag) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("addTag", newTag, resolve);
|
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add a tag to a monitor asynchronously
|
||||||
|
* @param {number} tagId ID of tag to add
|
||||||
|
* @param {number} monitorId ID of monitor to add tag to
|
||||||
|
* @param {string} value Value of tag
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
addMonitorTagAsync(tagId, monitorId, value) {
|
addMonitorTagAsync(tagId, monitorId, value) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Delete a tag from a monitor asynchronously
|
||||||
|
* @param {number} tagId ID of tag to remove
|
||||||
|
* @param {number} monitorId ID of monitor to remove tag from
|
||||||
|
* @param {string} value Value of tag
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
deleteMonitorTagAsync(tagId, monitorId, value) {
|
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** Handle pressing Enter key when inside the modal */
|
||||||
onEnter() {
|
onEnter() {
|
||||||
if (!this.validateDraftTag.invalid) {
|
if (!this.validateDraftTag.invalid) {
|
||||||
this.addDraftTag();
|
this.addDraftTag();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Submit the form data
|
||||||
|
* @param {number} monitorId ID of monitor this change affects
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
async submit(monitorId) {
|
async submit(monitorId) {
|
||||||
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
@@ -29,10 +29,12 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
/** Heading of the section */
|
||||||
heading: {
|
heading: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
/** Should the section be open by default? */
|
||||||
defaultOpen: {
|
defaultOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@@ -100,18 +100,22 @@ export default {
|
|||||||
this.getStatus();
|
this.getStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Show the dialog */
|
||||||
show() {
|
show() {
|
||||||
this.modal.show();
|
this.modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm enabling 2FA */
|
||||||
confirmEnableTwoFA() {
|
confirmEnableTwoFA() {
|
||||||
this.$refs.confirmEnableTwoFA.show();
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm disabling 2FA */
|
||||||
confirmDisableTwoFA() {
|
confirmDisableTwoFA() {
|
||||||
this.$refs.confirmDisableTwoFA.show();
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Prepare 2FA configuration */
|
||||||
prepare2FA() {
|
prepare2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
@@ -126,6 +130,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Save the current 2FA configuration */
|
||||||
save2FA() {
|
save2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
@@ -143,6 +148,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Disable 2FA for this user */
|
||||||
disable2FA() {
|
disable2FA() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
@@ -160,6 +166,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Verify the token generated by the user */
|
||||||
verifyToken() {
|
verifyToken() {
|
||||||
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -170,6 +177,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Get current status of 2FA */
|
||||||
getStatus() {
|
getStatus() {
|
||||||
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@@ -5,8 +5,17 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
monitor: Object,
|
/** Monitor this represents */
|
||||||
type: String,
|
monitor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Type of monitor */
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/** Is this a pill? */
|
||||||
pill: {
|
pill: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
13
src/components/notifications/AlertNow.vue
Normal file
13
src/components/notifications/AlertNow.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="alertnow-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="alertnow-webhook-url" v-model="$parent.notification.alertNowWebhookURL" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
<a href="https://service.opsnow.com/docs/alertnow/en/user-guide-alertnow-en.html#standard" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -8,6 +8,9 @@
|
|||||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input id="title" v-model="$parent.notification.title" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<i18n-t tag="p" keypath="Status:">
|
<i18n-t tag="p" keypath="Status:">
|
||||||
|
@@ -2,9 +2,6 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||||
<div class="form-text">
|
|
||||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
|
||||||
</div>
|
|
||||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Finb/Bark"
|
href="https://github.com/Finb/Bark"
|
||||||
@@ -12,4 +9,45 @@
|
|||||||
>{{ $t("here") }}</a>
|
>{{ $t("here") }}</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Group" class="form-label">{{ $t("Bark Group") }}</label>
|
||||||
|
<input id="Bark Group" v-model="$parent.notification.barkGroup" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Bark Sound" class="form-label">{{ $t("Bark Sound") }}</label>
|
||||||
|
<select id="Bark Sound" v-model="$parent.notification.barkSound" class="form-select" required>
|
||||||
|
<option value="alarm">alarm</option>
|
||||||
|
<option value="anticipate">anticipate</option>
|
||||||
|
<option value="bell">bell</option>
|
||||||
|
<option value="birdsong">birdsong</option>
|
||||||
|
<option value="bloom">bloom</option>
|
||||||
|
<option value="calypso">calypso</option>
|
||||||
|
<option value="chime">chime</option>
|
||||||
|
<option value="choo">choo</option>
|
||||||
|
<option value="descent">descent</option>
|
||||||
|
<option value="electronic">electronic</option>
|
||||||
|
<option value="fanfare">fanfare</option>
|
||||||
|
<option value="glass">glass</option>
|
||||||
|
<option value="gotosleep">gotosleep</option>
|
||||||
|
<option value="healthnotification">healthnotification</option>
|
||||||
|
<option value="horn">horn</option>
|
||||||
|
<option value="ladder">ladder</option>
|
||||||
|
<option value="mailsent">mailsent</option>
|
||||||
|
<option value="minuet">minuet</option>
|
||||||
|
<option value="multiwayinvitation">multiwayinvitation</option>
|
||||||
|
<option value="newmail">newmail</option>
|
||||||
|
<option value="newsflash">newsflash</option>
|
||||||
|
<option value="noir">noir</option>
|
||||||
|
<option value="paymentsuccess">paymentsuccess</option>
|
||||||
|
<option value="shake">shake</option>
|
||||||
|
<option value="sherwoodforest">sherwoodforest</option>
|
||||||
|
<option value="silence">silence</option>
|
||||||
|
<option value="spell">spell</option>
|
||||||
|
<option value="suspense">suspense</option>
|
||||||
|
<option value="telegraph">telegraph</option>
|
||||||
|
<option value="tiptoes">tiptoes</option>
|
||||||
|
<option value="typewriters">typewriters</option>
|
||||||
|
<option value="update">update</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-login" class="form-label">API Username</label>
|
<label for="clicksendsms-login" class="form-label">{{ $t("API Username") }}</label>
|
||||||
<div class="form-text">
|
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
|
||||||
{{ $t("apiCredentials") }}
|
|
||||||
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||||
</div>
|
</i18n-t>
|
||||||
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||||
<label for="clicksendsms-key" class="form-label">API Key</label>
|
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -16,15 +15,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
<label for="clicksendsms-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
|
||||||
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
<label for="clicksendsms-sender-name" class="form-label">{{ $t("From Name/Number") }} -
|
||||||
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">{{ $t("Read more") }}</a>
|
||||||
</label>
|
</label>
|
||||||
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
<div class="form-text">Leave blank to use a shared sender number.</div>
|
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
40
src/components/notifications/HomeAssistant.vue
Normal file
40
src/components/notifications/HomeAssistant.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
|
||||||
|
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
|
||||||
|
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
|
||||||
|
<p>
|
||||||
|
{{ $t("Trigger type:") }} <code>Event</code><br />
|
||||||
|
{{ $t("Event type:") }} <code>call_service</code><br />
|
||||||
|
{{ $t("Event data:") }}
|
||||||
|
</p>
|
||||||
|
<pre>domain: notify
|
||||||
|
service: mobile_app_my_phone # change to your device name
|
||||||
|
service_data:
|
||||||
|
title: Uptime Kuma
|
||||||
|
data:
|
||||||
|
status: 0 # 0=down 1=up
|
||||||
|
# name: Optional Uptime Kuma Monitor Name to filter by</pre>
|
||||||
|
<p>
|
||||||
|
{{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -7,7 +7,7 @@
|
|||||||
<b>{{ $t("Basic Settings") }}</b>
|
<b>{{ $t("Basic Settings") }}</b>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<div class="mb-3" style="margin-top: 12px;">
|
<div class="mb-3" style="margin-top: 12px;">
|
||||||
<label for="line-user-id" class="form-label">User ID</label>
|
<label for="line-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||||
|
9
src/components/notifications/LineNotify.vue
Normal file
9
src/components/notifications/LineNotify.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||||
|
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
|
||||||
|
</div>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
|
||||||
|
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
30
src/components/notifications/Ntfy.vue
Normal file
30
src/components/notifications/Ntfy.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
|
||||||
|
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
|
||||||
|
this.$parent.notification.ntfyPriority = 5;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-version" class="form-label">Octopush API Version</label>
|
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
|
||||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||||
<option value="2">Octopush (endpoint: api.octopush.com)</option>
|
<option value="2">{{ $t("octopush") }} ({{ $t("endpoint") }}: api.octopush.com)</option>
|
||||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
<option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("octopushLegacyHint") }}
|
{{ $t("octopushLegacyHint") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-key" class="form-label">API KEY</label>
|
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
|
||||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
|
||||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
45
src/components/notifications/PagerDuty.vue
Normal file
45
src/components/notifications/PagerDuty.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-key" class="form-label">{{ $t("Integration Key") }}</label>
|
||||||
|
<HiddenInput id="pagerduty-integration-key" v-model="$parent.notification.pagerdutyIntegrationKey" :required="true" autocomplete="false"></HiddenInput>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetPagerDutyKey" class="form-text">
|
||||||
|
<a href="https://support.pagerduty.com/docs/services-and-integrations" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-integration-url" class="form-label">{{ $t("Integration URL") }}</label>
|
||||||
|
<input id="pagerduty-integration-url" v-model="$parent.notification.pagerdutyIntegrationUrl" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||||
|
<select id="pagerduty-priority" v-model="$parent.notification.pagerdutyPriority" class="form-select">
|
||||||
|
<option value="info">{{ $t("info") }}</option>
|
||||||
|
<option value="warning" selected="selected">{{ $t("warning") }}</option>
|
||||||
|
<option value="error">{{ $t("error") }}</option>
|
||||||
|
<option value="critical">{{ $t("critical") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pagerduty-resolve" class="form-label">{{ $t("Auto resolve or acknowledged") }}</label>
|
||||||
|
<select id="pagerduty-resolve" v-model="$parent.notification.pagerdutyAutoResolve" class="form-select">
|
||||||
|
<option value="0" selected="selected">{{ $t("do nothing") }}</option>
|
||||||
|
<option value="acknowledge">{{ $t("auto acknowledged") }}</option>
|
||||||
|
<option value="resolve">{{ $t("auto resolve") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.pagerdutyIntegrationUrl === "undefined") {
|
||||||
|
this.$parent.notification.pagerdutyIntegrationUrl = "https://events.pagerduty.com/v2/enqueue";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-login" class="form-label">API LOGIN</label>
|
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
|
||||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||||
<label for="promosms-key" class="form-label">API PASSWORD</label>
|
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
|
||||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@@ -18,28 +18,29 @@
|
|||||||
</select>
|
</select>
|
||||||
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
||||||
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||||
<option>pushover</option>
|
<option value="pushover">{{ $t("pushoversounds pushover") }}</option>
|
||||||
<option>bike</option>
|
<option value="bike">{{ $t("pushoversounds bike") }}</option>
|
||||||
<option>bugle</option>
|
<option value="bugle">{{ $t("pushoversounds bugle") }}</option>
|
||||||
<option>cashregister</option>
|
<option value="cashregister">{{ $t("pushoversounds cashregister") }}</option>
|
||||||
<option>classical</option>
|
<option value="classical">{{ $t("pushoversounds classical") }}</option>
|
||||||
<option>cosmic</option>
|
<option value="cosmic">{{ $t("pushoversounds cosmic") }}</option>
|
||||||
<option>falling</option>
|
<option value="falling">{{ $t("pushoversounds falling") }}</option>
|
||||||
<option>gamelan</option>
|
<option value="gamelan">{{ $t("pushoversounds gamelan") }}</option>
|
||||||
<option>incoming</option>
|
<option value="incoming">{{ $t("pushoversounds incoming") }}</option>
|
||||||
<option>intermission</option>
|
<option value="intermission">{{ $t("pushoversounds intermission") }}</option>
|
||||||
<option>mechanical</option>
|
<option value="magic">{{ $t("pushoversounds magic") }}</option>
|
||||||
<option>pianobar</option>
|
<option value="mechanical">{{ $t("pushoversounds mechanical") }}</option>
|
||||||
<option>siren</option>
|
<option value="pianobar">{{ $t("pushoversounds pianobar") }}</option>
|
||||||
<option>spacealarm</option>
|
<option value="siren">{{ $t("pushoversounds siren") }}</option>
|
||||||
<option>tugboat</option>
|
<option value="spacealarm">{{ $t("pushoversounds spacealarm") }}</option>
|
||||||
<option>alien</option>
|
<option value="tugboat">{{ $t("pushoversounds tugboat") }}</option>
|
||||||
<option>climb</option>
|
<option value="alien">{{ $t("pushoversounds alien") }}</option>
|
||||||
<option>persistent</option>
|
<option value="climb">{{ $t("pushoversounds climb") }}</option>
|
||||||
<option>echo</option>
|
<option value="persistent">{{ $t("pushoversounds persistent") }}</option>
|
||||||
<option>updown</option>
|
<option value="echo">{{ $t("pushoversounds echo") }}</option>
|
||||||
<option>vibrate</option>
|
<option value="updown">{{ $t("pushoversounds updown") }}</option>
|
||||||
<option>none</option>
|
<option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
|
||||||
|
<option value="none">{{ $t("pushoversounds none") }}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
|
||||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="push-api-key" class="form-label">API_KEY</label>
|
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -1,36 +1,41 @@
|
|||||||
import STMP from "./SMTP.vue";
|
import Alerta from "./Alerta.vue";
|
||||||
import Telegram from "./Telegram.vue";
|
import AlertNow from "./AlertNow.vue";
|
||||||
import Discord from "./Discord.vue";
|
|
||||||
import Webhook from "./Webhook.vue";
|
|
||||||
import Signal from "./Signal.vue";
|
|
||||||
import Gotify from "./Gotify.vue";
|
|
||||||
import Slack from "./Slack.vue";
|
|
||||||
import RocketChat from "./RocketChat.vue";
|
|
||||||
import Teams from "./Teams.vue";
|
|
||||||
import Pushover from "./Pushover.vue";
|
|
||||||
import Pushy from "./Pushy.vue";
|
|
||||||
import TechulusPush from "./TechulusPush.vue";
|
|
||||||
import Octopush from "./Octopush.vue";
|
|
||||||
import PromoSMS from "./PromoSMS.vue";
|
|
||||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
|
||||||
import LunaSea from "./LunaSea.vue";
|
|
||||||
import Feishu from "./Feishu.vue";
|
|
||||||
import Apprise from "./Apprise.vue";
|
|
||||||
import Pushbullet from "./Pushbullet.vue";
|
|
||||||
import Line from "./Line.vue";
|
|
||||||
import Mattermost from "./Mattermost.vue";
|
|
||||||
import Matrix from "./Matrix.vue";
|
|
||||||
import AliyunSMS from "./AliyunSms.vue";
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
import DingDing from "./DingDing.vue";
|
import Apprise from "./Apprise.vue";
|
||||||
import Bark from "./Bark.vue";
|
import Bark from "./Bark.vue";
|
||||||
import SerwerSMS from "./SerwerSMS.vue";
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
import Stackfield from "./Stackfield.vue";
|
import DingDing from "./DingDing.vue";
|
||||||
import WeCom from "./WeCom.vue";
|
import Discord from "./Discord.vue";
|
||||||
|
import Feishu from "./Feishu.vue";
|
||||||
import GoogleChat from "./GoogleChat.vue";
|
import GoogleChat from "./GoogleChat.vue";
|
||||||
import Gorush from "./Gorush.vue";
|
import Gorush from "./Gorush.vue";
|
||||||
import Alerta from "./Alerta.vue";
|
import Gotify from "./Gotify.vue";
|
||||||
|
import HomeAssistant from "./HomeAssistant.vue";
|
||||||
|
import Line from "./Line.vue";
|
||||||
|
import LineNotify from "./LineNotify.vue";
|
||||||
|
import LunaSea from "./LunaSea.vue";
|
||||||
|
import Matrix from "./Matrix.vue";
|
||||||
|
import Mattermost from "./Mattermost.vue";
|
||||||
|
import Ntfy from "./Ntfy.vue";
|
||||||
|
import Octopush from "./Octopush.vue";
|
||||||
import OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
|
import PagerDuty from "./PagerDuty.vue";
|
||||||
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
import PushDeer from "./PushDeer.vue";
|
import PushDeer from "./PushDeer.vue";
|
||||||
|
import Pushover from "./Pushover.vue";
|
||||||
|
import Pushy from "./Pushy.vue";
|
||||||
|
import RocketChat from "./RocketChat.vue";
|
||||||
|
import SerwerSMS from "./SerwerSMS.vue";
|
||||||
|
import Signal from "./Signal.vue";
|
||||||
|
import Slack from "./Slack.vue";
|
||||||
|
import Stackfield from "./Stackfield.vue";
|
||||||
|
import STMP from "./SMTP.vue";
|
||||||
|
import Teams from "./Teams.vue";
|
||||||
|
import TechulusPush from "./TechulusPush.vue";
|
||||||
|
import Telegram from "./Telegram.vue";
|
||||||
|
import Webhook from "./Webhook.vue";
|
||||||
|
import WeCom from "./WeCom.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
@@ -38,39 +43,44 @@ import PushDeer from "./PushDeer.vue";
|
|||||||
* @type { Record<string, any> }
|
* @type { Record<string, any> }
|
||||||
*/
|
*/
|
||||||
const NotificationFormList = {
|
const NotificationFormList = {
|
||||||
"telegram": Telegram,
|
"alerta": Alerta,
|
||||||
"webhook": Webhook,
|
"AlertNow": AlertNow,
|
||||||
"smtp": STMP,
|
|
||||||
"discord": Discord,
|
|
||||||
"teams": Teams,
|
|
||||||
"signal": Signal,
|
|
||||||
"gotify": Gotify,
|
|
||||||
"slack": Slack,
|
|
||||||
"rocket.chat": RocketChat,
|
|
||||||
"pushover": Pushover,
|
|
||||||
"pushy": Pushy,
|
|
||||||
"PushByTechulus": TechulusPush,
|
|
||||||
"octopush": Octopush,
|
|
||||||
"promosms": PromoSMS,
|
|
||||||
"clicksendsms": ClickSendSMS,
|
|
||||||
"lunasea": LunaSea,
|
|
||||||
"Feishu": Feishu,
|
|
||||||
"AliyunSMS": AliyunSMS,
|
"AliyunSMS": AliyunSMS,
|
||||||
"apprise": Apprise,
|
"apprise": Apprise,
|
||||||
"pushbullet": Pushbullet,
|
|
||||||
"line": Line,
|
|
||||||
"mattermost": Mattermost,
|
|
||||||
"matrix": Matrix,
|
|
||||||
"DingDing": DingDing,
|
|
||||||
"Bark": Bark,
|
"Bark": Bark,
|
||||||
"serwersms": SerwerSMS,
|
"clicksendsms": ClickSendSMS,
|
||||||
"stackfield": Stackfield,
|
"DingDing": DingDing,
|
||||||
"WeCom": WeCom,
|
"discord": Discord,
|
||||||
|
"Feishu": Feishu,
|
||||||
"GoogleChat": GoogleChat,
|
"GoogleChat": GoogleChat,
|
||||||
"gorush": Gorush,
|
"gorush": Gorush,
|
||||||
"alerta": Alerta,
|
"gotify": Gotify,
|
||||||
|
"HomeAssistant": HomeAssistant,
|
||||||
|
"line": Line,
|
||||||
|
"LineNotify": LineNotify,
|
||||||
|
"lunasea": LunaSea,
|
||||||
|
"matrix": Matrix,
|
||||||
|
"mattermost": Mattermost,
|
||||||
|
"ntfy": Ntfy,
|
||||||
|
"octopush": Octopush,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
|
"PagerDuty": PagerDuty,
|
||||||
|
"promosms": PromoSMS,
|
||||||
|
"pushbullet": Pushbullet,
|
||||||
|
"PushByTechulus": TechulusPush,
|
||||||
"PushDeer": PushDeer,
|
"PushDeer": PushDeer,
|
||||||
|
"pushover": Pushover,
|
||||||
|
"pushy": Pushy,
|
||||||
|
"rocket.chat": RocketChat,
|
||||||
|
"serwersms": SerwerSMS,
|
||||||
|
"signal": Signal,
|
||||||
|
"slack": Slack,
|
||||||
|
"smtp": STMP,
|
||||||
|
"stackfield": Stackfield,
|
||||||
|
"teams": Teams,
|
||||||
|
"telegram": Telegram,
|
||||||
|
"webhook": Webhook,
|
||||||
|
"WeCom": WeCom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationFormList;
|
export default NotificationFormList;
|
||||||
|
@@ -4,16 +4,21 @@
|
|||||||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
|
||||||
|
|
||||||
|
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
|
||||||
|
⚠️ {{ $t("Frontend Version do not match backend version!") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +51,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.update-link {
|
.update-link {
|
||||||
font-size: 0.9em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.frontend-version {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #cccccc;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -133,10 +133,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Show the confimation dialog confirming the configuration
|
||||||
|
* be imported
|
||||||
|
*/
|
||||||
confirmImport() {
|
confirmImport() {
|
||||||
this.$refs.confirmImport.show();
|
this.$refs.confirmImport.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Download a backup of the configuration */
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
@@ -157,6 +162,10 @@ export default {
|
|||||||
downloadItem.click();
|
downloadItem.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the specified backup file
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
importBackup() {
|
importBackup() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
let uploadItem = document.getElementById("import-backend").files;
|
let uploadItem = document.getElementById("import-backend").files;
|
||||||
|
48
src/components/settings/Docker.vue
Normal file
48
src/components/settings/Docker.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="dockerHost-list my-4">
|
||||||
|
<p v-if="$root.dockerHostList.length === 0">
|
||||||
|
{{ $t("Not available, please setup.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
|
<li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
|
||||||
|
{{ dockerHost.name }}<br>
|
||||||
|
<a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
||||||
|
{{ $t("Setup Docker Host") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DockerHostDialog ref="dockerHostDialog" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DockerHostDialog from "../../components/DockerHostDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DockerHostDialog,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@@ -178,10 +178,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Save the settings */
|
||||||
saveGeneral() {
|
saveGeneral() {
|
||||||
localStorage.timezone = this.$root.userTimezone;
|
localStorage.timezone = this.$root.userTimezone;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
},
|
},
|
||||||
|
/** Get the base URL of the application */
|
||||||
autoGetPrimaryBaseURL() {
|
autoGetPrimaryBaseURL() {
|
||||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
},
|
},
|
||||||
|
@@ -90,6 +90,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Get the current size of the database */
|
||||||
loadDatabaseSize() {
|
loadDatabaseSize() {
|
||||||
log.debug("monitorhistory", "load database size");
|
log.debug("monitorhistory", "load database size");
|
||||||
this.$root.getSocket().emit("getDatabaseSize", (res) => {
|
this.$root.getSocket().emit("getDatabaseSize", (res) => {
|
||||||
@@ -102,6 +103,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Request that the database is shrunk */
|
||||||
shrinkDatabase() {
|
shrinkDatabase() {
|
||||||
this.$root.getSocket().emit("shrinkDatabase", (res) => {
|
this.$root.getSocket().emit("shrinkDatabase", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -113,10 +115,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Show the dialog to confirm clearing stats */
|
||||||
confirmClearStatistics() {
|
confirmClearStatistics() {
|
||||||
this.$refs.confirmClearStatistics.show();
|
this.$refs.confirmClearStatistics.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Send the request to clear stats */
|
||||||
clearStatistics() {
|
clearStatistics() {
|
||||||
this.$root.clearStatistics((res) => {
|
this.$root.clearStatistics((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@@ -20,16 +20,92 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 pt-4">
|
||||||
|
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
|
||||||
|
<p>{{ $t("certificationExpiryDescription") }}</p>
|
||||||
|
<p>{{ $t("notificationDescription") }}</p>
|
||||||
|
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||||
|
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||||
|
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||||
|
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
||||||
|
<font-awesome-icon class="" icon="times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NotificationDialog ref="notificationDialog" />
|
<NotificationDialog ref="notificationDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotificationDialog from "../../components/NotificationDialog.vue";
|
import NotificationDialog from "../../components/NotificationDialog.vue";
|
||||||
|
import ActionInput from "../ActionInput.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog
|
NotificationDialog,
|
||||||
|
ActionInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Variable to store the input for new certificate expiry day.
|
||||||
|
*/
|
||||||
|
expiryNotifInput: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Remove a day from expiry notification days.
|
||||||
|
* @param {number} day The day to remove.
|
||||||
|
*/
|
||||||
|
removeExpiryNotifDay(day) {
|
||||||
|
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a new expiry notification day.
|
||||||
|
* Will verify:
|
||||||
|
* - day is not null or empty string.
|
||||||
|
* - day is a number.
|
||||||
|
* - day is > 0.
|
||||||
|
* - The day is not already in the list.
|
||||||
|
* @param {number} day The day number to add.
|
||||||
|
*/
|
||||||
|
addExpiryNotifDay(day) {
|
||||||
|
if (day != null && day !== "") {
|
||||||
|
const parsedDay = parseInt(day);
|
||||||
|
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
|
||||||
|
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
|
||||||
|
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
|
||||||
|
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
|
||||||
|
this.expiryNotifInput = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -37,10 +113,27 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../../assets/vars.scss";
|
@import "../../assets/vars.scss";
|
||||||
|
|
||||||
|
.btn-rm-expiry {
|
||||||
|
padding-left: 11px;
|
||||||
|
padding-right: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: $dark-bg2;
|
background-color: $dark-bg2;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cert-exp-days .cert-exp-day-row {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
border-bottom: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-exp-days .cert-exp-day-row:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -68,7 +68,9 @@
|
|||||||
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
|
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
|
||||||
{{ $t("The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.") }}
|
{{ $t("The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.") }}
|
||||||
|
|
||||||
<div class="mt-3">
|
<p class="mt-2">{{ $t("disableCloudflaredNoAuthMsg") }}</p>
|
||||||
|
|
||||||
|
<div v-if="!settings.disableAuth" class="mt-3">
|
||||||
<label for="current-password2" class="form-label">
|
<label for="current-password2" class="form-label">
|
||||||
{{ $t("Current Password") }}
|
{{ $t("Current Password") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -89,6 +91,51 @@
|
|||||||
{{ $t("For example: nginx, Apache and Traefik.") }} <br />
|
{{ $t("For example: nginx, Apache and Traefik.") }} <br />
|
||||||
{{ $t("Please read") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
|
{{ $t("Please read") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4 class="my-4">{{ $t("HTTP Headers") }}</h4>
|
||||||
|
<div class="my-3">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("Trust Proxy") }}
|
||||||
|
</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="trustProxyYes"
|
||||||
|
v-model="settings.trustProxy"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="trustProxyYes"
|
||||||
|
:value="true"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="trustProxyYes">
|
||||||
|
{{ $t("Yes") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="trustProxyNo"
|
||||||
|
v-model="settings.trustProxy"
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="flexRadioDefault"
|
||||||
|
:value="false"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="trustProxyNo">
|
||||||
|
{{ $t("No") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("trustProxyDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit" @click="saveSettings()">
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -108,7 +155,15 @@ export default {
|
|||||||
return this.$root.cloudflared;
|
return this.$root.cloudflared;
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
@@ -120,14 +175,17 @@ export default {
|
|||||||
this.$root.getSocket().emit(prefix + "leave");
|
this.$root.getSocket().emit(prefix + "leave");
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Start the Cloudflare tunnel */
|
||||||
start() {
|
start() {
|
||||||
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
|
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
|
||||||
},
|
},
|
||||||
|
/** Stop the Cloudflare tunnel */
|
||||||
stop() {
|
stop() {
|
||||||
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
|
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** Remove the token for the Cloudflare tunnel */
|
||||||
removeToken() {
|
removeToken() {
|
||||||
this.$root.getSocket().emit(prefix + "removeToken");
|
this.$root.getSocket().emit(prefix + "removeToken");
|
||||||
this.cloudflareTunnelToken = "";
|
this.cloudflareTunnelToken = "";
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
|
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h5 class="my-4">{{ $t("Change Password") }}</h5>
|
<h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5>
|
||||||
<form class="mb-3" @submit.prevent="savePassword">
|
<form class="mb-3" @submit.prevent="savePassword">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="current-password" class="form-label">
|
<label for="current-password" class="form-label">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||||
<h5 class="my-4">
|
<h5 class="my-4 settings-subheading">
|
||||||
{{ $t("Two Factor Authentication") }}
|
{{ $t("Two Factor Authentication") }}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<h5 class="my-4">{{ $t("Advanced") }}</h5>
|
<h5 class="my-4 settings-subheading">{{ $t("Advanced") }}</h5>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||||
@@ -90,156 +90,11 @@
|
|||||||
<TwoFADialog ref="TwoFADialog" />
|
<TwoFADialog ref="TwoFADialog" />
|
||||||
|
|
||||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||||
<template v-if="$i18n.locale === 'es-ES' ">
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
|
<p v-html="$t('disableauth.message1')"></p>
|
||||||
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<p>Por favor usar con cuidado.</p>
|
<p v-html="$t('disableauth.message2')"></p>
|
||||||
</template>
|
<p>{{ $t("Please use this option carefully!") }}</p>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'pt-BR' ">
|
|
||||||
<p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p>
|
|
||||||
<p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p>
|
|
||||||
<p>Por favor, utilize isso com cautela.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'zh-HK' ">
|
|
||||||
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
|
||||||
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
|
||||||
<p>請小心使用。</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'zh-CN' ">
|
|
||||||
<p>是否确定 <strong>取消登录验证</strong>?</p>
|
|
||||||
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能,如 Cloudflare Access</p>
|
|
||||||
<p>请谨慎使用!</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'zh-TW' ">
|
|
||||||
<p>你是否要<strong>取消登入驗證</strong>?</p>
|
|
||||||
<p>此功能是設計給已有<strong>第三方認證</strong>的使用者,例如 Cloudflare Access。</p>
|
|
||||||
<p>請謹慎使用。</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'de-DE' ">
|
|
||||||
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
|
|
||||||
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
|
|
||||||
<p>Bitte mit Vorsicht nutzen.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'sl-SI' ">
|
|
||||||
<p>Ali ste prepričani, da želite onemogočiti <strong>avtentikacijo</strong>?</p>
|
|
||||||
<p>Namenjen je <strong>nekomu, ki ima pred programom Uptime Kuma vklopljeno zunanje preverjanje pristnosti</strong>, na primer Cloudflare Access.</p>
|
|
||||||
<p>Uporabljajte previdno.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'sr' ">
|
|
||||||
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
|
|
||||||
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
|
|
||||||
<p>Молим Вас користите ово са пажњом.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'sr-latn' ">
|
|
||||||
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
|
|
||||||
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
|
|
||||||
<p>Molim Vas koristite ovo sa pažnjom.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="$i18n.locale === 'hr-HR' ">
|
|
||||||
<p>Jeste li sigurni da želite <strong>isključiti autentikaciju</strong>?</p>
|
|
||||||
<p>To je za <strong>korisnike koji imaju vanjsku autentikaciju stranice</strong> ispred Uptime Kume, poput usluge Cloudflare Access.</p>
|
|
||||||
<p>Pažljivo koristite ovu opciju.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'tr-TR' ">
|
|
||||||
<p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p>
|
|
||||||
<p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p>
|
|
||||||
<p>Lütfen dikkatli kullanın.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'ko-KR' ">
|
|
||||||
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
|
|
||||||
<p>이 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong>을 Uptime Kuma 앞에 둔 사용자를 위한 기능이에요.</p>
|
|
||||||
<p>신중하게 사용하세요.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'pl' ">
|
|
||||||
<p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p>
|
|
||||||
<p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p>
|
|
||||||
<p>Proszę używać ostrożnie.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'et-EE' ">
|
|
||||||
<p>Kas soovid <strong>lülitada autentimise välja</strong>?</p>
|
|
||||||
<p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p>
|
|
||||||
<p>Palun kasuta vastutustundlikult.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'it-IT' ">
|
|
||||||
<p><strong>Disabilitare l'autenticazione?</strong></p>
|
|
||||||
<p><strong>Questa opzione è per chi un sistema di autenticazione gestito da terze parti</strong> messo davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p>
|
|
||||||
<p>Utilizzare con attenzione!</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'id-ID' ">
|
|
||||||
<p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
|
|
||||||
<p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
|
|
||||||
<p>Gunakan dengan hati-hati.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
|
||||||
<p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p>
|
|
||||||
<p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p>
|
|
||||||
<p>Пожалуйста, используйте с осторожностью.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'uk-UA' ">
|
|
||||||
<p>Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?</p>
|
|
||||||
<p>Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.</p>
|
|
||||||
<p>Будь ласка, використовуйте з обережністю.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'fa' ">
|
|
||||||
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
|
|
||||||
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
|
|
||||||
<p>لطفا از این امکان با دقت استفاده کنید.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'bg-BG' ">
|
|
||||||
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
|
|
||||||
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p>
|
|
||||||
<p>Моля, използвайте с повишено внимание.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'hu' ">
|
|
||||||
<p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p>
|
|
||||||
<p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p>
|
|
||||||
<p>Használja megfontoltan!</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'nb-NO' ">
|
|
||||||
<p>Er du sikker på at du vil <strong>deaktiver autentisering</strong>?</p>
|
|
||||||
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
|
|
||||||
<p>Vennligst vær forsiktig.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'cs-CZ' ">
|
|
||||||
<p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p>
|
|
||||||
<p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p>
|
|
||||||
<p>Používejte ji prosím s rozmyslem.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'vi-VN' ">
|
|
||||||
<p>Bạn có muốn <strong>TẮT XÁC THỰC</strong> không?</p>
|
|
||||||
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng có thể truy cập và cướp quyền điều khiển.</p>
|
|
||||||
<p>Vui lòng <strong>cẩn thận</strong>.</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- English (en) -->
|
|
||||||
<template v-else>
|
|
||||||
<p>Are you sure want to <strong>disable authentication</strong>?</p>
|
|
||||||
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
|
|
||||||
<p>Please use this option carefully!</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="current-password2" class="form-label">
|
<label for="current-password2" class="form-label">
|
||||||
@@ -297,6 +152,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** Check new passwords match before saving them */
|
||||||
savePassword() {
|
savePassword() {
|
||||||
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||||
this.invalidPassword = true;
|
this.invalidPassword = true;
|
||||||
@@ -314,6 +170,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Disable authentication for web app access */
|
||||||
disableAuth() {
|
disableAuth() {
|
||||||
this.settings.disableAuth = true;
|
this.settings.disableAuth = true;
|
||||||
|
|
||||||
@@ -326,6 +183,7 @@ export default {
|
|||||||
}, this.password.currentPassword);
|
}, this.password.currentPassword);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Enable authentication for web app access */
|
||||||
enableAuth() {
|
enableAuth() {
|
||||||
this.settings.disableAuth = false;
|
this.settings.disableAuth = false;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
@@ -340,15 +198,3 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/vars.scss";
|
|
||||||
|
|
||||||
h5::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 50%;
|
|
||||||
padding-top: 8px;
|
|
||||||
border-bottom: 1px solid $dark-border-color;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@@ -9,7 +9,9 @@ const languageList = {
|
|||||||
"nl-NL": "Nederlands",
|
"nl-NL": "Nederlands",
|
||||||
"nb-NO": "Norsk",
|
"nb-NO": "Norsk",
|
||||||
"es-ES": "Español",
|
"es-ES": "Español",
|
||||||
|
"eu": "Euskara",
|
||||||
"fa": "Farsi",
|
"fa": "Farsi",
|
||||||
|
"pt-PT": "Português (Portugal)",
|
||||||
"pt-BR": "Português (Brasileiro)",
|
"pt-BR": "Português (Brasileiro)",
|
||||||
"fr-FR": "Français (France)",
|
"fr-FR": "Français (France)",
|
||||||
"hu": "Magyar",
|
"hu": "Magyar",
|
||||||
@@ -31,6 +33,7 @@ const languageList = {
|
|||||||
"vi-VN": "Tiếng Việt",
|
"vi-VN": "Tiếng Việt",
|
||||||
"zh-TW": "繁體中文 (台灣)",
|
"zh-TW": "繁體中文 (台灣)",
|
||||||
"uk-UA": "Український",
|
"uk-UA": "Український",
|
||||||
|
"th-TH": "ไทย",
|
||||||
};
|
};
|
||||||
|
|
||||||
let messages = {
|
let messages = {
|
||||||
|
@@ -81,6 +81,7 @@ library.add(
|
|||||||
faUndo,
|
faUndo,
|
||||||
faPlusCircle,
|
faPlusCircle,
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
|
faLink,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
@@ -4,8 +4,7 @@
|
|||||||
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
|
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. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
|
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
|
||||||
4. Your language file should be filled in. You can translate now.
|
4. Your language file should be filled in. You can translate now.
|
||||||
5. Translate `src/components/settings/Security.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
5. Add it into `languageList` constant.
|
||||||
6. Add it into `languageList` constant.
|
6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||||
7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
|
||||||
|
|
||||||
If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
If you do not have programming skills, let me know in [the issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
||||||
|
@@ -12,15 +12,15 @@ export default {
|
|||||||
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
|
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
|
||||||
pauseDashboardHome: "Пауза",
|
pauseDashboardHome: "Пауза",
|
||||||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
||||||
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
|
deleteNotificationMsg: "Наистина ли желаете да изтриете това известие за всички монитори?",
|
||||||
resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
|
resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
|
||||||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
||||||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
||||||
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
|
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
|
||||||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
||||||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
||||||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
||||||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известяване със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известяване.",
|
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
|
||||||
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
||||||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
||||||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
||||||
@@ -55,8 +55,7 @@ export default {
|
|||||||
Current: "Текущ",
|
Current: "Текущ",
|
||||||
Uptime: "Достъпност",
|
Uptime: "Достъпност",
|
||||||
"Cert Exp.": "Вал. сертификат",
|
"Cert Exp.": "Вал. сертификат",
|
||||||
days: "дни",
|
day: "ден | дни",
|
||||||
day: "ден",
|
|
||||||
"-day": "-дни",
|
"-day": "-дни",
|
||||||
hour: "час",
|
hour: "час",
|
||||||
"-hour": "-часa",
|
"-hour": "-часa",
|
||||||
@@ -76,9 +75,9 @@ export default {
|
|||||||
"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: "Автоматично",
|
||||||
@@ -90,13 +89,16 @@ export default {
|
|||||||
"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": "Включи удостоверяване",
|
||||||
|
"disableauth.message1": "Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?",
|
||||||
|
"disableauth.message2": "Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.",
|
||||||
|
"Please use this option carefully!": "Моля, използвайте с повишено внимание.",
|
||||||
Logout: "Изход от профила",
|
Logout: "Изход от профила",
|
||||||
Leave: "Отказ",
|
Leave: "Отказ",
|
||||||
"I understand, please disable": "Разбирам. Моля, изключи",
|
"I understand, please disable": "Разбирам. Моля, изключи",
|
||||||
@@ -109,7 +111,7 @@ export default {
|
|||||||
Login: "Вход",
|
Login: "Вход",
|
||||||
"No Monitors, please": "Все още няма монитори. Моля, добавете поне ",
|
"No Monitors, please": "Все още няма монитори. Моля, добавете поне ",
|
||||||
"add one": "един.",
|
"add one": "един.",
|
||||||
"Notification Type": "Тип известяване",
|
"Notification Type": "Тип известие",
|
||||||
Email: "Имейл",
|
Email: "Имейл",
|
||||||
Test: "Тест",
|
Test: "Тест",
|
||||||
"Certificate Info": "Информация за сертификат",
|
"Certificate Info": "Информация за сертификат",
|
||||||
@@ -131,9 +133,9 @@ export default {
|
|||||||
Events: "Събития",
|
Events: "Събития",
|
||||||
Heartbeats: "Проверки",
|
Heartbeats: "Проверки",
|
||||||
"Auto Get": "Авт. попълване",
|
"Auto Get": "Авт. попълване",
|
||||||
backupDescription: "Можете да архивирате всички монитори и всички известявания в JSON файл.",
|
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
|
||||||
backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.",
|
backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.",
|
||||||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
backupDescription3: "Чувствителни данни, като токен кодове за известия, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||||
alertNoFile: "Моля, изберете файл за импортиране.",
|
alertNoFile: "Моля, изберете файл за импортиране.",
|
||||||
alertWrongFileType: "Моля, изберете JSON файл.",
|
alertWrongFileType: "Моля, изберете JSON файл.",
|
||||||
"Clear all statistics": "Изтрий цялата статистика",
|
"Clear all statistics": "Изтрий цялата статистика",
|
||||||
@@ -145,7 +147,7 @@ export default {
|
|||||||
"Setup 2FA": "Настройка 2FA",
|
"Setup 2FA": "Настройка 2FA",
|
||||||
"Enable 2FA": "Включи 2FA",
|
"Enable 2FA": "Включи 2FA",
|
||||||
"Disable 2FA": "Изключи 2FA",
|
"Disable 2FA": "Изключи 2FA",
|
||||||
"2FA Settings": "Настройки 2FA",
|
"2FA Settings": "Настройка за 2FA",
|
||||||
"Two Factor Authentication": "Двуфакторно удостоверяване",
|
"Two Factor Authentication": "Двуфакторно удостоверяване",
|
||||||
Active: "Активно",
|
Active: "Активно",
|
||||||
Inactive: "Неактивно",
|
Inactive: "Неактивно",
|
||||||
@@ -202,7 +204,7 @@ export default {
|
|||||||
"Push URL": "Генериран Push URL адрес",
|
"Push URL": "Генериран Push URL адрес",
|
||||||
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
||||||
pushOptionalParams: "Допълнителни, но не задължителни параметри: {0}",
|
pushOptionalParams: "Допълнителни, но не задължителни параметри: {0}",
|
||||||
defaultNotificationName: "Моето {notification} известяване ({number})",
|
defaultNotificationName: "Моето {notification} известие ({number})",
|
||||||
here: "тук",
|
here: "тук",
|
||||||
Required: "Задължително поле",
|
Required: "Задължително поле",
|
||||||
"Bot Token": "Бот токен",
|
"Bot Token": "Бот токен",
|
||||||
@@ -252,7 +254,7 @@ export default {
|
|||||||
"Notification Sound": "Звуков сигнал",
|
"Notification Sound": "Звуков сигнал",
|
||||||
"More info on:": "Повече информация на: {0}",
|
"More info on:": "Повече информация на: {0}",
|
||||||
pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.",
|
pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.",
|
||||||
pushoverDesc2: "Ако желаете да изпратите известявания до различни устройства, попълнете полето Устройство.",
|
pushoverDesc2: "Ако желаете да изпратите известия до различни устройства, попълнете полето Устройство.",
|
||||||
"SMS Type": "SMS тип",
|
"SMS Type": "SMS тип",
|
||||||
octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)",
|
octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)",
|
||||||
octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)",
|
octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)",
|
||||||
@@ -275,7 +277,7 @@ export default {
|
|||||||
lineDevConsoleTo: "Line - Конзола за разработчици - {0}",
|
lineDevConsoleTo: "Line - Конзола за разработчици - {0}",
|
||||||
"Basic Settings": "Основни настройки",
|
"Basic Settings": "Основни настройки",
|
||||||
"User ID": "Потребител ID",
|
"User ID": "Потребител ID",
|
||||||
"Messaging API": "API за известяване",
|
"Messaging API": "API за съобщаване",
|
||||||
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
|
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
|
||||||
"Icon URL": "URL адрес за иконка",
|
"Icon URL": "URL адрес за иконка",
|
||||||
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
|
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
|
||||||
@@ -291,7 +293,7 @@ export default {
|
|||||||
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
|
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
|
||||||
"Internal Room Id": "ID на вътрешна стая",
|
"Internal Room Id": "ID на вътрешна стая",
|
||||||
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
|
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||||
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известяванията. Токен код за достъп ще получите изпълнявайки {0}",
|
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
|
||||||
Method: "Метод",
|
Method: "Метод",
|
||||||
Body: "Съобщение",
|
Body: "Съобщение",
|
||||||
Headers: "Хедъри",
|
Headers: "Хедъри",
|
||||||
@@ -299,7 +301,7 @@ export default {
|
|||||||
HeadersInvalidFormat: "Заявените хедъри не са валидни JSON: ",
|
HeadersInvalidFormat: "Заявените хедъри не са валидни JSON: ",
|
||||||
BodyInvalidFormat: "Заявеното съобщение не е валиден JSON: ",
|
BodyInvalidFormat: "Заявеното съобщение не е валиден JSON: ",
|
||||||
"Monitor History": "История на мониторите",
|
"Monitor History": "История на мониторите",
|
||||||
clearDataOlderThan: "Ще се съхранява {0} дни.",
|
clearDataOlderThan: "Ще се съхранява за {0} дни.",
|
||||||
records: "записа",
|
records: "записа",
|
||||||
"One record": "Един запис",
|
"One record": "Един запис",
|
||||||
steamApiKeyDescription: "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
|
steamApiKeyDescription: "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
|
||||||
@@ -308,12 +310,12 @@ export default {
|
|||||||
PasswordsDoNotMatch: "Паролите не съвпадат.",
|
PasswordsDoNotMatch: "Паролите не съвпадат.",
|
||||||
"Current User": "Текущ потребител",
|
"Current User": "Текущ потребител",
|
||||||
recent: "Скорошни",
|
recent: "Скорошни",
|
||||||
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не нужно.",
|
shrinkDatabaseDescription: "Инициира \"VACUUM\" за \"SQLite\" база данни. Ако Вашата база данни е създадена след версия 1.10.0, \"AUTO_VACUUM\" функцията е активна и това действие не е нужно.",
|
||||||
Done: "Готово",
|
Done: "Готово",
|
||||||
Info: "Информация",
|
Info: "Информация",
|
||||||
Security: "Сигурност",
|
Security: "Сигурност",
|
||||||
"Steam API Key": "Steam API ключ",
|
"Steam API Key": "Steam API ключ",
|
||||||
"Shrink Database": "Редуциране база данни",
|
"Shrink Database": "Редуцирай базата данни",
|
||||||
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониторитане...",
|
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониторитане...",
|
||||||
"Pick Accepted Status Codes...": "Изберете статус кодове, които да се считат за успешен отговор...",
|
"Pick Accepted Status Codes...": "Изберете статус кодове, които да се считат за успешен отговор...",
|
||||||
Default: "По подразбиране",
|
Default: "По подразбиране",
|
||||||
@@ -378,7 +380,7 @@ export default {
|
|||||||
deleteProxyMsg: "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
|
deleteProxyMsg: "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
|
||||||
proxyDescription: "За да функционират трябва да бъдат зададени към монитор.",
|
proxyDescription: "За да функционират трябва да бъдат зададени към монитор.",
|
||||||
enableProxyDescription: "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
|
enableProxyDescription: "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
|
||||||
setAsDefaultProxyDescription: "Това проки ще бъде включено по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
|
setAsDefaultProxyDescription: "Това прокси ще бъде включено по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
|
||||||
"Certificate Chain": "Верига на сертификата",
|
"Certificate Chain": "Верига на сертификата",
|
||||||
Valid: "Валиден",
|
Valid: "Валиден",
|
||||||
Invalid: "Невалиден",
|
Invalid: "Невалиден",
|
||||||
@@ -422,6 +424,7 @@ export default {
|
|||||||
Next: "Следващ",
|
Next: "Следващ",
|
||||||
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
|
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
|
||||||
"No Proxy": "Без прокси",
|
"No Proxy": "Без прокси",
|
||||||
|
Authentication: "Удостоверяване",
|
||||||
"HTTP Basic Auth": "HTTP основно удостоверяване",
|
"HTTP Basic Auth": "HTTP основно удостоверяване",
|
||||||
"New Status Page": "Нова статус страница",
|
"New Status Page": "Нова статус страница",
|
||||||
"Page Not Found": "Страницата не е открита",
|
"Page Not Found": "Страницата не е открита",
|
||||||
@@ -449,7 +452,7 @@ export default {
|
|||||||
Customize: "Персонализирай",
|
Customize: "Персонализирай",
|
||||||
"Custom Footer": "Персонализиран долен колонтитул",
|
"Custom Footer": "Персонализиран долен колонтитул",
|
||||||
"Custom CSS": "Потребителски CSS",
|
"Custom CSS": "Потребителски CSS",
|
||||||
"Domain Name Expiry Notification": "Известяване при изтичащ домейн",
|
"Domain Name Expiry Notification": "Известие при изтичащ домейн",
|
||||||
Proxy: "Прокси",
|
Proxy: "Прокси",
|
||||||
"Date Created": "Дата на създаване",
|
"Date Created": "Дата на създаване",
|
||||||
onebotHttpAddress: "OneBot HTTP адрес",
|
onebotHttpAddress: "OneBot HTTP адрес",
|
||||||
@@ -464,4 +467,99 @@ export default {
|
|||||||
"Domain Names": "Домейни",
|
"Domain Names": "Домейни",
|
||||||
signedInDisp: "Вписан като {0}",
|
signedInDisp: "Вписан като {0}",
|
||||||
signedInDispDisabled: "Удостоверяването е изключено.",
|
signedInDispDisabled: "Удостоверяването е изключено.",
|
||||||
|
"Certificate Expiry Notification": "Известие за изтичане валидността на сертификата",
|
||||||
|
"API Username": "API Потребител",
|
||||||
|
"API Key": "API Ключ",
|
||||||
|
"Recipient Number": "Номер на получателя",
|
||||||
|
"From Name/Number": "От Име/Номер",
|
||||||
|
"Leave blank to use a shared sender number.": "Оставете празно, за да използвате споделен номер на подател.",
|
||||||
|
"Octopush API Version": "Octopush API версия",
|
||||||
|
"Legacy Octopush-DM": "Octopush-DM старa версия",
|
||||||
|
endpoint: "крайна точка",
|
||||||
|
octopushAPIKey: "\"API ключ\" от HTTP API удостоверяване в контролния панел",
|
||||||
|
octopushLogin: "\"Вписване\" от HTTP API удостоверяване в контролния панел",
|
||||||
|
promosmsLogin: "API Потребителско име",
|
||||||
|
promosmsPassword: "API Парола",
|
||||||
|
"pushoversounds pushover": "Pushover (по подразбиране)",
|
||||||
|
"pushoversounds bike": "Велосипед",
|
||||||
|
"pushoversounds bugle": "Тромпет",
|
||||||
|
"pushoversounds cashregister": "Касов апарат",
|
||||||
|
"pushoversounds classical": "Класическа музика",
|
||||||
|
"pushoversounds cosmic": "Космически",
|
||||||
|
"pushoversounds falling": "Падащ",
|
||||||
|
"pushoversounds gamelan": "Игра в мрежа",
|
||||||
|
"pushoversounds incoming": "Входящ",
|
||||||
|
"pushoversounds intermission": "Прекъсване",
|
||||||
|
"pushoversounds magic": "Магия",
|
||||||
|
"pushoversounds mechanical": "Механичен",
|
||||||
|
"pushoversounds pianobar": "Пиано бар",
|
||||||
|
"pushoversounds siren": "Сирена",
|
||||||
|
"pushoversounds spacealarm": "Космическа аларма",
|
||||||
|
"pushoversounds tugboat": "Буксир",
|
||||||
|
"pushoversounds alien": "Извънземна аларма (дълъг)",
|
||||||
|
"pushoversounds climb": "Изкачване (дълъг)",
|
||||||
|
"pushoversounds persistent": "Постоянен (дълъг)",
|
||||||
|
"pushoversounds echo": "Pushover ехо (дълъг)",
|
||||||
|
"pushoversounds updown": "Горе долу (дълъг)",
|
||||||
|
"pushoversounds vibrate": "Само вибрация",
|
||||||
|
"pushoversounds none": "Без (тих)",
|
||||||
|
pushyAPIKey: "Таен API ключ",
|
||||||
|
pushyToken: "Токен на устройство",
|
||||||
|
"Show update if available": "Покажи актуализация, ако е налична",
|
||||||
|
"Also check beta release": "Проверявай и за бета версии",
|
||||||
|
"Using a Reverse Proxy?": "Използвате ревърс прокси?",
|
||||||
|
"Check how to config it for WebSocket": "Проверете как да го конфигурирате за WebSocket",
|
||||||
|
"Steam Game Server": "Steam Game сървър",
|
||||||
|
"Most likely causes:": "Най-вероятни причини:",
|
||||||
|
"The resource is no longer available.": "Ресурсът вече не е наличен.",
|
||||||
|
"There might be a typing error in the address.": "Възможно е да е допусната грешка при изписването на адреса.",
|
||||||
|
"What you can try:": "Може да опитате:",
|
||||||
|
"Retype the address.": "Повторно въвеждане на адреса.",
|
||||||
|
"Go back to the previous page.": "Да се върнете към предишната страница.",
|
||||||
|
"Coming Soon": "Очаквайте скоро",
|
||||||
|
wayToGetClickSendSMSToken: "Може да получите API потребителско име и API ключ от {0} .",
|
||||||
|
dnsPortDescription: "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
|
||||||
|
error: "грешка",
|
||||||
|
critical: "критично",
|
||||||
|
wayToGetPagerDutyKey: "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук трябва да потърсите \"Events API V2\". Повече информация {0}",
|
||||||
|
"Integration Key": "Ключ за интегриране",
|
||||||
|
"Integration URL": "URL адрес за интеграция",
|
||||||
|
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
|
||||||
|
"do nothing": "не прави нищо",
|
||||||
|
"auto acknowledged": "автоматично потвърждаване",
|
||||||
|
"auto resolve": "автоматично разрешаване",
|
||||||
|
"Connection String": "Стринг за връзка",
|
||||||
|
Query: "Заявка",
|
||||||
|
settingsCertificateExpiry: "Изтичане валидността на TLS сертификата",
|
||||||
|
certificationExpiryDescription: "HTTPS мониторите ще задействат известие, ако е наличен изтичащ TLS сертификат, през следващите:",
|
||||||
|
"ntfy Topic": "ntfy Тема",
|
||||||
|
Domain: "Домейн",
|
||||||
|
Workstation: "Работна станция",
|
||||||
|
disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.",
|
||||||
|
wayToGetLineNotifyToken: "Може да получите токен код за достъп от {0}",
|
||||||
|
resendEveryXTimes: "Изпращай повторно на всеки {0} пъти",
|
||||||
|
resendDisabled: "Повторното изпращане е изключено",
|
||||||
|
"Resend Notification if Down X times consequently": "Повторно изпращане на известие, ако е недостъпен X пъти последователно",
|
||||||
|
"Bark Group": "Bark група",
|
||||||
|
"Bark Sound": "Bark звук",
|
||||||
|
"HTTP Headers": "HTTP хедъри",
|
||||||
|
"Trust Proxy": "Trust Proxy",
|
||||||
|
HomeAssistant: "Home Assistant",
|
||||||
|
RadiusSecret: "Radius таен код",
|
||||||
|
RadiusSecretDescription: "Споделен таен код между клиент и сървър",
|
||||||
|
RadiusCalledStationId: "Повиквана станция ID",
|
||||||
|
RadiusCalledStationIdDescription: "Идентификатор на повикваното устройство",
|
||||||
|
RadiusCallingStationId: "Повикваща станция ID",
|
||||||
|
RadiusCallingStationIdDescription: "Идентификатор на повикващото устройство",
|
||||||
|
"Setup Docker Host": "Настройка на Docker хост",
|
||||||
|
"Connection Type": "Тип свързване",
|
||||||
|
"Docker Daemon": "Docker демон",
|
||||||
|
deleteDockerHostMsg: "Сигурни ли сте, че желаете да изтриете този Docker хост за всички монитори?",
|
||||||
|
socket: "Сокет",
|
||||||
|
tcp: "TCP / HTTP",
|
||||||
|
"Docker Container": "Docker контейнер",
|
||||||
|
"Container Name / ID": "Име на контейнер / ID",
|
||||||
|
"Docker Host": "Docker хост",
|
||||||
|
"Docker Hosts": "Docker хостове",
|
||||||
|
trustProxyDescription: "Trust 'X-Forwarded-*' headers. Ако искате да получавате правилния IP адрес на клиента, а Uptime Kuma е зад системи като Nginx или Apache, трябва да разрешите тази опция.",
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user