mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 13:36:55 +08:00
Compare commits
440 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8d8d5987e7 | ||
|
1fa90bffaa | ||
|
67f221d3c7 | ||
|
6eceb4c744 | ||
|
3e4154dfb5 | ||
|
fbc8828ddc | ||
|
3677aa639f | ||
|
aaddfa1786 | ||
|
6d65d248f4 | ||
|
87a4748b40 | ||
|
182bdf13a7 | ||
|
0f66e5cfc5 | ||
|
fe5ae46013 | ||
|
756f317f82 | ||
|
cc9fe26584 | ||
|
99818aa370 | ||
|
682265fe9c | ||
|
4406e51ab6 | ||
|
c3d5be5a5e | ||
|
dfe12c99c1 | ||
|
bb69851160 | ||
|
6c9323351d | ||
|
8e9fa20e57 | ||
|
da7c29f4b9 | ||
|
e545d48583 | ||
|
8d84d8f891 | ||
|
74acc2cea7 | ||
|
ba4a4089eb | ||
|
122631c91b | ||
|
1e12f25c4b | ||
|
9e10296290 | ||
|
87e213085f | ||
|
d9038f1da2 | ||
|
3fb2d0ce68 | ||
|
50a33e3b45 | ||
|
ce9a97a107 | ||
|
0afa3a2c21 | ||
|
51512b6f5f | ||
|
3d79e841c9 | ||
|
d2af42e579 | ||
|
d6e3bd2282 | ||
|
70fbe9a2d9 | ||
|
c669e7eaba | ||
|
9a9fd41f62 | ||
|
50b868e751 | ||
|
a856780066 | ||
|
266b03fbf7 | ||
|
662c97dcde | ||
|
1ebf752f1a | ||
|
69bb6ef290 | ||
|
720ea850e1 | ||
|
7fb55b8875 | ||
|
4786514e9f | ||
|
32c9dfbb31 | ||
|
d3d4363031 | ||
|
6f352a6e3c | ||
|
3fb06a8ba4 | ||
|
b3a5a5b0ba | ||
|
a4cad3db65 | ||
|
5fa9b33c79 | ||
|
f6a984b671 | ||
|
23a63213aa | ||
|
a9bb8ae6a1 | ||
|
27d4c3c194 | ||
|
439f45d91e | ||
|
66598a81cc | ||
|
45a6f364e1 | ||
|
95391be2ab | ||
|
5f533b9091 | ||
|
29920f6b60 | ||
|
ad1bb93730 | ||
|
0a5a6e6a4b | ||
|
fe0fc63843 | ||
|
5b2e1f6086 | ||
|
8c7ee94769 | ||
|
0834770509 | ||
|
a71814c80b | ||
|
17073fd786 | ||
|
15c00d9158 | ||
|
d2b34f9285 | ||
|
a96a515087 | ||
|
2eab919ae0 | ||
|
c09b67b94d | ||
|
61c737c53c | ||
|
2820118eba | ||
|
469e8f6fd6 | ||
|
780cb0a145 | ||
|
1c8b3ce451 | ||
|
e8ef63e0a3 | ||
|
4591adc05e | ||
|
5f6aa32844 | ||
|
a8e170f6a8 | ||
|
6aaf984f29 | ||
|
76a39f1388 | ||
|
34c0fa59a8 | ||
|
0664217a09 | ||
|
b0e9c5bcb4 | ||
|
0b572df3d0 | ||
|
bcc02c1680 | ||
|
795d5f586f | ||
|
7ee98d989c | ||
|
7dc1e84e44 | ||
|
6350c43cc3 | ||
|
fd95d41d9f | ||
|
7665bae927 | ||
|
09e38269c6 | ||
|
6681f49a58 | ||
|
3fc2ba3d76 | ||
|
78e12ab899 | ||
|
2b8c049e7b | ||
|
f0ac3c82d2 | ||
|
803029a9e4 | ||
|
c3f1cebeab | ||
|
5d9814559c | ||
|
9ef45a9c7e | ||
|
7f78cc8d0f | ||
|
ca84044809 | ||
|
9eaa4ab846 | ||
|
c3122a9807 | ||
|
f6498caa9a | ||
|
3a0bc80016 | ||
|
cbbc503f8e | ||
|
5be51abd8f | ||
|
6ec219908b | ||
|
a6fdd272a6 | ||
|
1b5e723f60 | ||
|
4bdada36a9 | ||
|
250c2bdd6d | ||
|
8e49d84050 | ||
|
112d72da47 | ||
|
9b8f01cfc6 | ||
|
579e07ded4 | ||
|
a04878fa84 | ||
|
2955abb5d9 | ||
|
8230cfe13f | ||
|
8b463e70df | ||
|
392f8275b3 | ||
|
783ec97004 | ||
|
4f4fe39c9b | ||
|
611d214a32 | ||
|
7a0cebf5bb | ||
|
54aa68ec58 | ||
|
217637aa1f | ||
|
ab22961538 | ||
|
8ba8de07ae | ||
|
a34b2623c8 | ||
|
79920b5f2c | ||
|
72783fd94c | ||
|
80322cbfe7 | ||
|
7e0272077b | ||
|
b2ff6b6098 | ||
|
512ff09cca | ||
|
e8f4fabcd0 | ||
|
f679c1cbaf | ||
|
2ab06f87b8 | ||
|
76db55b657 | ||
|
1693873f4a | ||
|
53bfdb7dad | ||
|
db05b506f3 | ||
|
0752f810f1 | ||
|
200d233607 | ||
|
7274913686 | ||
|
1300448bed | ||
|
3f67326fce | ||
|
8b48f9bd22 | ||
|
76348cae5d | ||
|
0b32adadb8 | ||
|
3425a27a0e | ||
|
fff5fd8e9e | ||
|
1d6670ed9a | ||
|
795411a11c | ||
|
3234aec5b3 | ||
|
afe91078c4 | ||
|
2009f7097d | ||
|
c14b71a4a7 | ||
|
35fe9690e8 | ||
|
ad2062713c | ||
|
379656335d | ||
|
fc9b4617ca | ||
|
40d301457a | ||
|
9902c181bc | ||
|
069c811af8 | ||
|
f9311e4e7f | ||
|
34abff4724 | ||
|
d7a230ac15 | ||
|
9da2a01a74 | ||
|
74da02da2c | ||
|
97360dab26 | ||
|
56dad006b5 | ||
|
47092258f8 | ||
|
a0838543b9 | ||
|
ccb8736b3d | ||
|
f351b423c5 | ||
|
ab6b97d77a | ||
|
0e16e88fa8 | ||
|
c746923018 | ||
|
df8754827d | ||
|
2c02dad1f9 | ||
|
600ec6d171 | ||
|
728636de06 | ||
|
8be7fd5c45 | ||
|
59ff9fa293 | ||
|
a654e409df | ||
|
76f1f34a0a | ||
|
35aca46b68 | ||
|
6a925c7ed4 | ||
|
8b9b86b478 | ||
|
7c4b5f189b | ||
|
a6f9762c4d | ||
|
a0e4e96160 | ||
|
fcbeed55bf | ||
|
9b5abf9bb1 | ||
|
73f4b8e177 | ||
|
e6669fbb9e | ||
|
e66a2d362d | ||
|
c2148fc0f1 | ||
|
6e3a904aaa | ||
|
50175b733c | ||
|
2617e1f4d8 | ||
|
91ee39ec60 | ||
|
4711aeb355 | ||
|
db9e97d859 | ||
|
7199bb5ead | ||
|
bb87972543 | ||
|
ea723c7595 | ||
|
e205adfd7b | ||
|
063d64eec8 | ||
|
5abf2e7597 | ||
|
7c83bed273 | ||
|
1cc788da68 | ||
|
54ffef8b7a | ||
|
44aa837a9d | ||
|
f47f7758f9 | ||
|
ddcd3c2a19 | ||
|
7df9698e5d | ||
|
471333ad03 | ||
|
d313966d80 | ||
|
8205f90f3d | ||
|
02247c4174 | ||
|
8352d9abbe | ||
|
2d7767c905 | ||
|
c52b9b63d5 | ||
|
e55193270d | ||
|
9d39071a91 | ||
|
389d247463 | ||
|
a170694a83 | ||
|
5969e913b5 | ||
|
0d280f5edb | ||
|
660b969178 | ||
|
c26622aa39 | ||
|
a7674755ba | ||
|
6d7cb44acc | ||
|
400fc13cf2 | ||
|
db14768229 | ||
|
3d27561e5a | ||
|
b80c88d9a0 | ||
|
862672731f | ||
|
7fee4a7ea7 | ||
|
c4f78d776e | ||
|
f8f9f59464 | ||
|
934685637a | ||
|
295ccba44b | ||
|
8cd5bad44c | ||
|
d003abcd60 | ||
|
f6d1a82989 | ||
|
651b525d06 | ||
|
66769e1c79 | ||
|
3e25f0e9d9 | ||
|
39c7b5e748 | ||
|
b709857e19 | ||
|
9e10f7d35f | ||
|
6caae725f9 | ||
|
c387fb264e | ||
|
4b0a8087a2 | ||
|
08c7a37052 | ||
|
0969be5981 | ||
|
2ed8f12dc0 | ||
|
2da77d8448 | ||
|
f8322de5da | ||
|
e21a18a593 | ||
|
14b7688b70 | ||
|
d953ba7c60 | ||
|
ef1604675b | ||
|
83fc844e8e | ||
|
22a7acbaf6 | ||
|
201bba63d7 | ||
|
0d6888c586 | ||
|
a06fc14213 | ||
|
31b4a5c33e | ||
|
951ece8a65 | ||
|
a49df29a87 | ||
|
08de0090dc | ||
|
97df200d6c | ||
|
d5b17a3778 | ||
|
2f0d994c83 | ||
|
e683033f4e | ||
|
c3c576bd13 | ||
|
c62732fa9c | ||
|
d823493fad | ||
|
463c385cf1 | ||
|
59cccf8c50 | ||
|
403202d4d4 | ||
|
2583dbe0e0 | ||
|
ca479673e7 | ||
|
573c7faddd | ||
|
7a4432de1e | ||
|
94a6c1a344 | ||
|
ee9e48fe53 | ||
|
a4aee49b03 | ||
|
e330875c80 | ||
|
44eba70b78 | ||
|
c6dab944d1 | ||
|
0f4bc5850b | ||
|
f37190a73d | ||
|
29f96fae93 | ||
|
331ae5ec20 | ||
|
8ee34c7904 | ||
|
4f07c2ea9a | ||
|
0b1d0d22ff | ||
|
24facc79d7 | ||
|
9f9c1007d7 | ||
|
70a6f0313c | ||
|
1d05df6ec9 | ||
|
b54bbc302d | ||
|
dd283423ab | ||
|
6006038689 | ||
|
a7b50c3630 | ||
|
0ddbac5109 | ||
|
0f440596c8 | ||
|
5a0fcebd6e | ||
|
87678ea92d | ||
|
0bf0cfa104 | ||
|
a7cf14c663 | ||
|
325ce72a71 | ||
|
9d00bd9029 | ||
|
f6a168282e | ||
|
acf6f1883b | ||
|
f44f951e40 | ||
|
dea9b05d49 | ||
|
230a9bfaf9 | ||
|
d761d54d0e | ||
|
e37621853e | ||
|
f2cec60481 | ||
|
2212cf9c08 | ||
|
f7562e00c1 | ||
|
678e248966 | ||
|
c1aebef005 | ||
|
1ef4562905 | ||
|
69a7c4dab4 | ||
|
2c3880a326 | ||
|
f4e885982d | ||
|
dcf6dd8b32 | ||
|
093cfec8dd | ||
|
f13dcb3c5b | ||
|
1a5ffd4755 | ||
|
a2f9e9e9c4 | ||
|
62712f5cc4 | ||
|
f0f1847ad8 | ||
|
0aeaf87f5b | ||
|
5ca0dd628d | ||
|
4a106431f3 | ||
|
d164b6ccce | ||
|
da74391c3e | ||
|
850563442a | ||
|
242e494cb5 | ||
|
4faa409027 | ||
|
f842fb409e | ||
|
72dd894856 | ||
|
0b0417e064 | ||
|
fe35d95441 | ||
|
da131a5156 | ||
|
926c15ea40 | ||
|
87e874406a | ||
|
0e288ea92d | ||
|
8a4a87716f | ||
|
d01fa9bcfc | ||
|
ec8c1cad31 | ||
|
fb0fa2a843 | ||
|
f5c6d422d5 | ||
|
ea4240455f | ||
|
ce30ee74e9 | ||
|
66b5f157eb | ||
|
5fe8a0917a | ||
|
848296b77a | ||
|
edfaacbb5f | ||
|
bb8385d690 | ||
|
b95404b6e0 | ||
|
cc351b2883 | ||
|
3c06f249ca | ||
|
31648dc5e8 | ||
|
1197cfa11e | ||
|
fd8c95d64e | ||
|
58240aceef | ||
|
e8b814733d | ||
|
dcc7799108 | ||
|
424f20f8e1 | ||
|
d4ff5d8b32 | ||
|
899b33b3a9 | ||
|
1b8b33c4c3 | ||
|
d34ed00841 | ||
|
f9c177b150 | ||
|
9952463350 | ||
|
5837c353b7 | ||
|
d5b32ffbb8 | ||
|
61936ae5eb | ||
|
299506ce45 | ||
|
ca8d4ab61b | ||
|
ffbdf97478 | ||
|
cc25787878 | ||
|
17453a8812 | ||
|
f1a151b4a1 | ||
|
d35b205fcc | ||
|
2a34e41d8c | ||
|
41d32bb9dd | ||
|
0b9e410ea5 | ||
|
904ed326ca | ||
|
778995a4fb | ||
|
ee60d74910 | ||
|
b0acda52f9 | ||
|
e9cd9be03a | ||
|
6ae279c7f3 | ||
|
9c32adfb55 | ||
|
d346afd33b | ||
|
3bf380c684 | ||
|
dca5c59982 | ||
|
8f9a973ede | ||
|
b98ec0c3d4 | ||
|
ebf362754c | ||
|
7d70c4d8cd | ||
|
532ad3044c | ||
|
2dd392e609 | ||
|
c13cc62d3d | ||
|
8e1e4b9204 | ||
|
66f9ad5754 | ||
|
b604910bbb | ||
|
2f6c5963c5 | ||
|
7b92166d18 | ||
|
1341d220ed | ||
|
1952e34110 | ||
|
b9a6088f50 |
@@ -2,8 +2,12 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/data
|
||||
/out
|
||||
/test
|
||||
/kubernetes
|
||||
/.do
|
||||
**/.dockerignore
|
||||
/private
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/docker-compose*
|
||||
@@ -20,6 +24,11 @@ yarn.lock
|
||||
app.json
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
CNAME
|
||||
install.sh
|
||||
SECURITY.md
|
||||
tsconfig.json
|
||||
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
|
18
.eslintrc.js
18
.eslintrc.js
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
@@ -16,8 +17,10 @@ module.exports = {
|
||||
requireConfigFile: false,
|
||||
},
|
||||
rules: {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"camelcase": ["warn", {
|
||||
"properties": "never"
|
||||
"properties": "never",
|
||||
"ignoreImports": true
|
||||
}],
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
@@ -31,11 +34,12 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
quotes: ["warn", "double"],
|
||||
//semi: ['off', 'never'],
|
||||
semi: "warn",
|
||||
"vue/html-indent": ["warn", 4], // default: 2
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||
"no-multi-spaces": ["error", {
|
||||
ignoreEOLComments: true,
|
||||
}],
|
||||
@@ -81,4 +85,12 @@ module.exports = {
|
||||
"one-var": ["error", "never"],
|
||||
"max-statements-per-line": ["error", { "max": 1 }]
|
||||
},
|
||||
}
|
||||
"overrides": [
|
||||
{
|
||||
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||
"rules": {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@@ -12,6 +12,7 @@ Please search in Issues without filters: https://github.com/louislam/uptime-kuma
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
Docker Version:
|
||||
Node.js Version (Without Docker only):
|
||||
OS:
|
||||
Browser:
|
||||
|
||||
|
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -23,12 +24,13 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Info**
|
||||
- Uptime Kuma Version:
|
||||
- Using Docker?: Yes/No
|
||||
- OS:
|
||||
- Browser:
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
Docker Version:
|
||||
Node.js Version (Without Docker only):
|
||||
OS:
|
||||
Browser:
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
@@ -36,3 +38,5 @@ If applicable, add screenshots to help explain your problem.
|
||||
**Error Log**
|
||||
It is easier for us to find out the problem.
|
||||
|
||||
Docker: `docker logs <container id>`
|
||||
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,4 +7,7 @@ dist-ssr
|
||||
|
||||
/data
|
||||
!/data/.gitkeep
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
/private
|
||||
/out
|
||||
|
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
|
||||
@@ -20,13 +20,13 @@ If you are not sure, feel free to create an empty pull request draft first.
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
|
||||
### *️⃣ Requires one more reviewer
|
||||
### *️⃣ Requires one more reviewer
|
||||
|
||||
I do not have such knowledge to test it.
|
||||
|
||||
- Add k8s supports
|
||||
- Add k8s supports
|
||||
|
||||
### *️⃣ Low Priority
|
||||
### *️⃣ Low Priority
|
||||
|
||||
It changed my current workflow and require further studies.
|
||||
|
||||
@@ -41,9 +41,9 @@ It changed my current workflow and require further studies.
|
||||
|
||||
# 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.
|
||||
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||
@@ -52,8 +52,8 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
||||
|
||||
# Coding Styles
|
||||
|
||||
- Follow .editorconfig
|
||||
- Follow eslint
|
||||
- Follow `.editorconfig`
|
||||
- Follow ESLint
|
||||
|
||||
## Name convention
|
||||
|
||||
@@ -62,12 +62,13 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
||||
- CSS/SCSS: dash-type
|
||||
|
||||
# Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
|
||||
- IDE that supports EditorConfig and ESLint (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
|
||||
# Install dependencies
|
||||
# Install dependencies
|
||||
|
||||
```bash
|
||||
npm install --dev
|
||||
@@ -75,32 +76,29 @@ npm install --dev
|
||||
|
||||
For npm@7, you need --legacy-peer-deps
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install --legacy-peer-deps --dev
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
|
||||
(2021-09-23 Update)
|
||||
|
||||
```bash
|
||||
npm run start-server
|
||||
|
||||
# Or
|
||||
|
||||
node server/server.js
|
||||
npm run start-server-dev
|
||||
```
|
||||
|
||||
It binds to 0.0.0.0:3001 by default.
|
||||
|
||||
It binds to `0.0.0.0:3001` by default.
|
||||
|
||||
## Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
|
||||
# Frontend Dev
|
||||
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to `0.0.0.0:3000` by default.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -108,7 +106,7 @@ npm run dev
|
||||
|
||||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
|
||||
|
||||
You can use Vue Devtool Chrome extension for debugging.
|
||||
You can use Vue.js devtools Chrome extension for debugging.
|
||||
|
||||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
|
||||
|
||||
@@ -118,8 +116,7 @@ localStorage.dev = "dev";
|
||||
|
||||
So that the frontend will try to connect websocket server in 3001.
|
||||
|
||||
Alternately, you can specific NODE_ENV to "development".
|
||||
|
||||
Alternately, you can specific `NODE_ENV` to "development".
|
||||
|
||||
## Build the frontend
|
||||
|
||||
@@ -131,22 +128,17 @@ npm run build
|
||||
|
||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||
|
||||
The router in "src/main.js"
|
||||
The router is in `src/router.js`
|
||||
|
||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
||||
|
||||
The data and socket logic in "src/mixins/socket.js"
|
||||
The data and socket logic are in `src/mixins/socket.js`.
|
||||
|
||||
# Database Migration
|
||||
|
||||
1. create `patch{num}.sql` in `./db/`
|
||||
1. update `latestVersion` in `./server/database.js`
|
||||
1. Create `patch{num}.sql` in `./db/`
|
||||
2. Update `latestVersion` in `./server/database.js`
|
||||
|
||||
# Unit Test
|
||||
|
||||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
22
README.md
22
README.md
@@ -20,13 +20,13 @@ It is a 5 minutes live demo, all data will be deleted after that. The server is
|
||||
|
||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
||||
* 20 seconds interval.
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
@@ -44,6 +44,9 @@ Browse to http://localhost:3001 after started.
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
|
||||
```bash
|
||||
# Update your npm to the latest version
|
||||
npm install npm -g
|
||||
|
||||
git clone https://github.com/louislam/uptime-kuma.git
|
||||
cd uptime-kuma
|
||||
npm run setup
|
||||
@@ -64,7 +67,6 @@ If you need more options or need to browse via a reserve proxy, please read:
|
||||
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||
|
||||
|
||||
## 🆙 How to Update
|
||||
|
||||
Please read:
|
||||
@@ -106,9 +108,21 @@ Telegram Notification Sample:
|
||||
|
||||
If you love this project, please consider giving me a ⭐.
|
||||
|
||||
## 🗣️ Discussion
|
||||
|
||||
### Issues Page
|
||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
### Subreddit
|
||||
My Reddit account: louislamlam
|
||||
You can mention me if you ask question on Reddit.
|
||||
https://www.reddit.com/r/UptimeKuma/
|
||||
|
||||
## Contribute
|
||||
|
||||
If you want to report a bug or request a new feature. Free feel to open a new issue.
|
||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
||||
|
@@ -10,5 +10,6 @@ currently being supported with security updates.
|
||||
| 1.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
Please report security issues to uptime@kuma.pet.
|
||||
|
||||
https://github.com/louislam/uptime-kuma/issues
|
||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||
|
BIN
db/demo_kuma.db
BIN
db/demo_kuma.db
Binary file not shown.
10
db/patch-2fa.sql
Normal file
10
db/patch-2fa.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 user
|
||||
ADD twofa_secret VARCHAR(64);
|
||||
|
||||
ALTER TABLE user
|
||||
ADD twofa_status BOOLEAN default 0 NOT NULL;
|
||||
|
||||
COMMIT;
|
7
db/patch-add-retry-interval-monitor.sql
Normal file
7
db/patch-add-retry-interval-monitor.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD retry_interval INTEGER default 0 not null;
|
||||
|
||||
COMMIT;
|
30
db/patch-group-table.sql
Normal file
30
db/patch-group-table.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table `group`
|
||||
(
|
||||
id INTEGER not null
|
||||
constraint group_pk
|
||||
primary key autoincrement,
|
||||
name VARCHAR(255) not null,
|
||||
created_date DATETIME default (DATETIME('now')) not null,
|
||||
public BOOLEAN default 0 not null,
|
||||
active BOOLEAN default 1 not null,
|
||||
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||
);
|
||||
|
||||
CREATE TABLE [monitor_group]
|
||||
(
|
||||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||
);
|
||||
|
||||
CREATE INDEX [fk]
|
||||
ON [monitor_group] (
|
||||
[monitor_id],
|
||||
[group_id]);
|
||||
|
||||
|
||||
COMMIT;
|
10
db/patch-improve-performance.sql
Normal file
10
db/patch-improve-performance.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;
|
||||
|
||||
-- For sendHeartbeatList
|
||||
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
|
||||
|
||||
-- For sendImportantHeartbeatList
|
||||
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
|
||||
|
||||
COMMIT;
|
18
db/patch-incident-table.sql
Normal file
18
db/patch-incident-table.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 incident
|
||||
(
|
||||
id INTEGER not null
|
||||
constraint incident_pk
|
||||
primary key autoincrement,
|
||||
title VARCHAR(255) not null,
|
||||
content TEXT not null,
|
||||
style VARCHAR(30) default 'warning' not null,
|
||||
created_date DATETIME default (DATETIME('now')) not null,
|
||||
last_updated_date DATETIME,
|
||||
pin BOOLEAN default 1 not null,
|
||||
active BOOLEAN default 1 not null
|
||||
);
|
||||
|
||||
COMMIT;
|
22
db/patch-setting-value-type.sql
Normal file
22
db/patch-setting-value-type.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Generated by Intellij IDEA
|
||||
create table setting_dg_tmp
|
||||
(
|
||||
id INTEGER
|
||||
primary key autoincrement,
|
||||
key VARCHAR(200) not null
|
||||
unique,
|
||||
value TEXT,
|
||||
type VARCHAR(20)
|
||||
);
|
||||
|
||||
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting;
|
||||
|
||||
drop table setting;
|
||||
|
||||
alter table setting_dg_tmp rename to setting;
|
||||
|
||||
|
||||
COMMIT;
|
19
db/patch10.sql
Normal file
19
db/patch10.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
CREATE TABLE tag (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
color VARCHAR(255) NOT NULL,
|
||||
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE monitor_tag (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
value TEXT,
|
||||
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
|
||||
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);
|
7
db/patch9.sql
Normal file
7
db/patch9.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE notification
|
||||
ADD is_default BOOLEAN default 0 NOT NULL;
|
||||
|
||||
COMMIT;
|
63
dockerfile
63
dockerfile
@@ -1,26 +1,57 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS release
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
npm install mapbox/node-sqlite3#593c9d && \
|
||||
apk del .build-deps && \
|
||||
rm -f /usr/bin/python
|
||||
|
||||
# Install apprise
|
||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
||||
RUN pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && npm run build && npm prune
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-buster-slim AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
RUN apt update && \
|
||||
apt --yes 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 && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
||||
# Upload the artifact to Github
|
||||
FROM node:14-buster-slim AS upload-artifact
|
||||
WORKDIR /
|
||||
RUN apt update && \
|
||||
apt --yes install curl file
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ARG TARGETARCH
|
||||
ARG PLATFORM=debian
|
||||
ARG VERSION=1.5.0
|
||||
|
||||
|
||||
COPY --from=build /app /app
|
||||
|
||||
RUN FILE=uptime-kuma.tar.gz
|
||||
RUN tar -czf $FILE app
|
||||
|
||||
RUN curl \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: $(file -b --mime-type $FILE)" \
|
||||
--data-binary @$FILE \
|
||||
"https://uploads.github.com/repos/louislam/uptime-kuma/releases/$VERSION/assets?name=$(basename $FILE)"
|
||||
|
||||
|
30
dockerfile-alpine
Normal file
30
dockerfile-alpine
Normal file
@@ -0,0 +1,30 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# 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 && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
21
extra/entrypoint.sh
Normal file
21
extra/entrypoint.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# set -e Exit the script if an error happens
|
||||
set -e
|
||||
PUID=${PUID=0}
|
||||
PGID=${PGID=0}
|
||||
|
||||
files_ownership () {
|
||||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
||||
# -R Recursively descends the specified directories
|
||||
# -c Like verbose but report only when a change is made
|
||||
chown -hRc "$PUID":"$PGID" /app/data
|
||||
}
|
||||
|
||||
echo "==> Performing startup jobs and maintenance tasks"
|
||||
files_ownership
|
||||
|
||||
echo "==> Starting application with user $PUID group $PGID"
|
||||
|
||||
# --clear-groups Clear supplementary groups.
|
||||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"
|
@@ -1,19 +1,34 @@
|
||||
let http = require("http");
|
||||
/*
|
||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||
*/
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
let client;
|
||||
|
||||
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
||||
client = require("https");
|
||||
} else {
|
||||
client = require("http");
|
||||
}
|
||||
|
||||
let options = {
|
||||
host: "localhost",
|
||||
port: "3001",
|
||||
timeout: 2000,
|
||||
host: process.env.HOST || "127.0.0.1",
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
timeout: 28 * 1000,
|
||||
};
|
||||
let request = http.request(options, (res) => {
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
|
||||
let request = client.request(options, (res) => {
|
||||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||
if (res.statusCode === 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
request.on("error", function (err) {
|
||||
console.log("ERROR");
|
||||
console.error("Health Check ERROR");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
request.end();
|
||||
|
@@ -6,12 +6,14 @@ const Database = require("../server/database");
|
||||
const { R } = require("redbean-node");
|
||||
const readline = require("readline");
|
||||
const { initJWTSecret } = require("../server/util-server");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
Database.init(args);
|
||||
await Database.connect();
|
||||
|
||||
try {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
// Need to use es6 to read language files
|
||||
// Need to use ES6 to read language files
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -14,6 +14,7 @@ const copyRecursiveSync = function (src, dest) {
|
||||
let exists = fs.existsSync(src);
|
||||
let stats = exists && fs.statSync(src);
|
||||
let isDirectory = exists && stats.isDirectory();
|
||||
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(dest);
|
||||
fs.readdirSync(src).forEach(function (childItemName) {
|
||||
@@ -24,8 +25,9 @@ const copyRecursiveSync = function (src, dest) {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
console.log(process.argv)
|
||||
const baseLangCode = process.argv[2] || "zh-HK";
|
||||
|
||||
console.log("Arguments:", process.argv)
|
||||
const baseLangCode = process.argv[2] || "en";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
@@ -33,46 +35,50 @@ copyRecursiveSync("../../src/languages", "./languages");
|
||||
const en = (await import("./languages/en.js")).default;
|
||||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
||||
const files = fs.readdirSync("./languages");
|
||||
console.log(files);
|
||||
console.log("Files:", files);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".js")) {
|
||||
console.log("Processing " + file);
|
||||
const lang = await import("./languages/" + file);
|
||||
if (!file.endsWith(".js")) {
|
||||
console.log("Skipping " + file)
|
||||
continue;
|
||||
}
|
||||
|
||||
let obj;
|
||||
console.log("Processing " + file);
|
||||
const lang = await import("./languages/" + file);
|
||||
|
||||
if (lang.default) {
|
||||
console.log("is js module");
|
||||
obj = lang.default;
|
||||
} else {
|
||||
console.log("empty file");
|
||||
obj = {
|
||||
languageName: "<Your Language name in your language (not in English)>"
|
||||
};
|
||||
}
|
||||
|
||||
// En first
|
||||
for (const key in en) {
|
||||
if (! obj[key]) {
|
||||
obj[key] = en[key];
|
||||
}
|
||||
let obj;
|
||||
|
||||
if (lang.default) {
|
||||
obj = lang.default;
|
||||
} else {
|
||||
console.log("Empty file");
|
||||
obj = {
|
||||
languageName: "<Your Language name in your language (not in English)>"
|
||||
};
|
||||
}
|
||||
|
||||
// En first
|
||||
for (const key in en) {
|
||||
if (! obj[key]) {
|
||||
obj[key] = en[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (baseLang !== en) {
|
||||
// Base second
|
||||
for (const key in baseLang) {
|
||||
if (! obj[key]) {
|
||||
obj[key] = key;
|
||||
}
|
||||
}
|
||||
|
||||
const code = "export default " + util.inspect(obj, {
|
||||
depth: null,
|
||||
});
|
||||
|
||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||
|
||||
}
|
||||
|
||||
const code = "export default " + util.inspect(obj, {
|
||||
depth: null,
|
||||
});
|
||||
|
||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||
}
|
||||
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
console.log("Done, fix the format by eslint now");
|
||||
console.log("Done. Fixing formatting by ESLint...");
|
||||
|
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="theme-color" id="theme-color" content="" />
|
||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
<title>Uptime Kuma</title>
|
||||
|
@@ -1,28 +1,32 @@
|
||||
# Uptime-Kuma K8s Deployment
|
||||
|
||||
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
||||
|
||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
|
||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
|
||||
|
||||
## What do I have to edit?
|
||||
|
||||
## What do i have to edit?
|
||||
You have to edit the ```ingressroute.yml``` to your needs.
|
||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
||||
|
||||
- host
|
||||
- secrets and secret names
|
||||
- Host
|
||||
- Secrets and secret names
|
||||
- (Cluster)Issuer (optional)
|
||||
- the Version in the Deployment-File
|
||||
- update:
|
||||
- change to newer version and run the above commands, it will update the pods one after another
|
||||
- The Version in the Deployment-File
|
||||
- Update:
|
||||
- Change to newer version and run the above commands, it will update the pods one after another
|
||||
|
||||
## How To use:
|
||||
## How To use
|
||||
|
||||
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
||||
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
||||
- Edit files mentioned above to your needs
|
||||
- run ```kustomize build > apply.yml```
|
||||
- run ```kubectl apply -f apply.yml```
|
||||
- Run ```kustomize build > apply.yml```
|
||||
- Run ```kubectl apply -f apply.yml```
|
||||
|
||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
||||
|
@@ -30,6 +30,9 @@ spec:
|
||||
command:
|
||||
- node
|
||||
- extra/healthcheck.js
|
||||
initialDelaySeconds: 180
|
||||
periodSeconds: 60
|
||||
timeoutSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
|
3930
package-lock.json
generated
3930
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.3",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -10,20 +10,26 @@
|
||||
"node": "14.*"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install --legacy-peer-deps",
|
||||
"update-legacy": "npm update --legacy-peer-deps",
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
"build": "vite build",
|
||||
"tsc": "tsc",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "npm run build-docker-alpine && npm run build-docker-debian",
|
||||
"build-docker-alpine": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.5.3 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push",
|
||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --target release . --push",
|
||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
||||
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.3-alpine --target release . --push",
|
||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.3-debian --target release . --push",
|
||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-alpine": "docker buildx build -f 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 --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.7.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@@ -32,61 +38,72 @@
|
||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix"
|
||||
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"@louislam/sqlite3": "^5.0.6",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.21.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.1.0",
|
||||
"bootstrap": "^5.1.1",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "^3.6.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"dayjs": "^1.10.7",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.3",
|
||||
"nodemailer": "^6.6.5",
|
||||
"notp": "^2.0.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"prom-client": "^13.2.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"redbean-node": "0.1.2",
|
||||
"socket.io": "^4.2.0",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"timezones-list": "^3.0.1",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"vue": "^3.2.8",
|
||||
"vue-chart-3": "^0.5.7",
|
||||
"vue": "next",
|
||||
"vue-chart-3": "^0.5.8",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-contenteditable": "^3.0.4",
|
||||
"vue-i18n": "^9.1.7",
|
||||
"vue-image-crop-upload": "^3.0.3",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"vue-qrcode": "^1.0.0",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-toastification": "^2.0.0-rc.1"
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@types/bootstrap": "^5.1.2",
|
||||
"@vitejs/plugin-legacy": "^1.5.2",
|
||||
"@vitejs/plugin-vue": "^1.6.0",
|
||||
"@vue/compiler-sfc": "^3.2.6",
|
||||
"core-js": "^3.17.0",
|
||||
"@babel/eslint-parser": "^7.15.7",
|
||||
"@types/bootstrap": "^5.1.6",
|
||||
"@vitejs/plugin-legacy": "^1.5.3",
|
||||
"@vitejs/plugin-vue": "^1.9.1",
|
||||
"@vue/compiler-sfc": "^3.2.16",
|
||||
"core-js": "^3.18.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dns2": "^2.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"sass": "^1.38.2",
|
||||
"eslint-plugin-vue": "^7.18.0",
|
||||
"sass": "^1.42.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.4.2",
|
||||
"vite": "^2.5.3"
|
||||
"typescript": "^4.4.3",
|
||||
"vite": "2.5.*"
|
||||
}
|
||||
}
|
||||
|
BIN
public/icon-192x192.png
Normal file
BIN
public/icon-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon-512x512.png
Normal file
BIN
public/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
19
public/manifest.json
Normal file
19
public/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Uptime Kuma",
|
||||
"short_name": "Uptime Kuma",
|
||||
"start_url": "/",
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
@@ -18,7 +18,7 @@ exports.startInterval = () => {
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.version = "1000.0.0"
|
||||
res.data.version = "1000.0.0";
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
|
88
server/client.js
Normal file
88
server/client.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* For Client Socket
|
||||
*/
|
||||
const { TimeLogger } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { io } = require("./server");
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
let list = await R.find("notification", " user_id = ? ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.export())
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
|
||||
timeLogger.print("Send Notification List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 100
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
let result = list.reverse();
|
||||
|
||||
if (toUser) {
|
||||
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
|
||||
} else {
|
||||
socket.emit("heartbeatList", monitorID, result, overwrite);
|
||||
}
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param socket
|
||||
* @param monitorID
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
*/
|
||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
AND important = 1
|
||||
ORDER BY time DESC
|
||||
LIMIT 500
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||
|
||||
if (toUser) {
|
||||
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
|
||||
} else {
|
||||
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
}
|
@@ -1,42 +1,121 @@
|
||||
const fs = require("fs");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { debug, sleep } = require("../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const knex = require("knex");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
*/
|
||||
class Database {
|
||||
|
||||
static templatePath = "./db/kuma.db"
|
||||
static templatePath = "./db/kuma.db";
|
||||
|
||||
/**
|
||||
* Data Dir (Default: ./data)
|
||||
*/
|
||||
static dataDir;
|
||||
|
||||
/**
|
||||
* User Upload Dir (Default: ./data/upload)
|
||||
*/
|
||||
static uploadDir;
|
||||
|
||||
static path;
|
||||
static latestVersion = 8;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
static patched = false;
|
||||
|
||||
/**
|
||||
* For Backup only
|
||||
*/
|
||||
static backupPath = null;
|
||||
|
||||
/**
|
||||
* Add patch filename in key
|
||||
* Values:
|
||||
* true: Add it regardless of order
|
||||
* false: Do nothing
|
||||
* { parents: []}: Need parents before add it
|
||||
*/
|
||||
static patchList = {
|
||||
"patch-setting-value-type.sql": true,
|
||||
"patch-improve-performance.sql": true,
|
||||
"patch-2fa.sql": true,
|
||||
"patch-add-retry-interval-monitor.sql": true,
|
||||
"patch-incident-table.sql": true,
|
||||
"patch-group-table.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
* The finally version should be 10 after merged tag feature
|
||||
* @deprecated Use patchList for any new feature
|
||||
*/
|
||||
static latestVersion = 10;
|
||||
|
||||
static noReject = true;
|
||||
static sqliteInstance = null;
|
||||
|
||||
static init(args) {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
Database.path = Database.dataDir + "kuma.db";
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
Database.uploadDir = Database.dataDir + "upload/";
|
||||
|
||||
if (! fs.existsSync(Database.uploadDir)) {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
static async connect() {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
R.setup("sqlite", {
|
||||
filename: Database.path,
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
|
||||
const knexInstance = knex({
|
||||
client: Dialect,
|
||||
connection: {
|
||||
filename: Database.path,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
}, {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
}
|
||||
});
|
||||
|
||||
R.setup(knexInstance);
|
||||
|
||||
if (process.env.SQL_LOG === "1") {
|
||||
R.debug(true);
|
||||
}
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
R.freeze(true);
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
|
||||
console.log("SQLite config:");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
console.log(await R.getAll("PRAGMA cache_size"));
|
||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
}
|
||||
|
||||
static async patch() {
|
||||
@@ -54,21 +133,9 @@ class Database {
|
||||
} else if (version > this.latestVersion) {
|
||||
console.info("Warning: Database version is newer than expected");
|
||||
} else {
|
||||
console.info("Database patch is needed")
|
||||
console.info("Database patch is needed");
|
||||
|
||||
console.info("Backup the db")
|
||||
const backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.copyFileSync(walPath, walPath + ".bak" + version);
|
||||
}
|
||||
this.backup(version);
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
try {
|
||||
@@ -79,18 +146,95 @@ class Database {
|
||||
console.info(`Patched ${sqlFile}`);
|
||||
await setSetting("database_version", i);
|
||||
}
|
||||
console.log("Database Patched Successfully");
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
console.error("Patch db failed!!! Restoring the backup")
|
||||
fs.copyFileSync(backupPath, Database.path);
|
||||
console.error(ex)
|
||||
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await this.patch2();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call it from patch() only
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2() {
|
||||
console.log("Database Patch 2.0 Process");
|
||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||
|
||||
if (! databasePatchedFiles) {
|
||||
databasePatchedFiles = {};
|
||||
}
|
||||
|
||||
debug("Patched files:");
|
||||
debug(databasePatchedFiles);
|
||||
|
||||
try {
|
||||
for (let sqlFilename in this.patchList) {
|
||||
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
|
||||
}
|
||||
|
||||
if (this.patched) {
|
||||
console.log("Database Patched Successfully");
|
||||
}
|
||||
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
|
||||
console.error(ex);
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used it patch2() only
|
||||
* @param sqlFilename
|
||||
* @param databasePatchedFiles
|
||||
*/
|
||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||
let value = this.patchList[sqlFilename];
|
||||
|
||||
if (! value) {
|
||||
console.log(sqlFilename + " skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if patched
|
||||
if (! databasePatchedFiles[sqlFilename]) {
|
||||
console.log(sqlFilename + " is not patched");
|
||||
|
||||
if (value.parents) {
|
||||
console.log(sqlFilename + " need parents");
|
||||
for (let parentSQLFilename of value.parents) {
|
||||
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||
|
||||
console.log(sqlFilename + " is patching");
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
databasePatchedFiles[sqlFilename] = true;
|
||||
console.log(sqlFilename + " is patched successfully");
|
||||
|
||||
} else {
|
||||
debug(sqlFilename + " is already patched, skip");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,12 +251,12 @@ class Database {
|
||||
// Remove all comments (--)
|
||||
let lines = text.split("\n");
|
||||
lines = lines.filter((line) => {
|
||||
return ! line.startsWith("--")
|
||||
return ! line.startsWith("--");
|
||||
});
|
||||
|
||||
// Split statements by semicolon
|
||||
// Filter out empty line
|
||||
text = lines.join("\n")
|
||||
text = lines.join("\n");
|
||||
|
||||
let statements = text.split(";")
|
||||
.map((statement) => {
|
||||
@@ -120,7 +264,7 @@ class Database {
|
||||
})
|
||||
.filter((statement) => {
|
||||
return statement !== "";
|
||||
})
|
||||
});
|
||||
|
||||
for (let statement of statements) {
|
||||
await R.exec(statement);
|
||||
@@ -136,10 +280,96 @@ class Database {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async close() {
|
||||
if (this.sqliteInstance) {
|
||||
this.sqliteInstance.close();
|
||||
const listener = (reason, p) => {
|
||||
Database.noReject = false;
|
||||
};
|
||||
process.addListener("unhandledRejection", listener);
|
||||
|
||||
console.log("Closing DB");
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
await R.close();
|
||||
await sleep(2000);
|
||||
|
||||
if (Database.noReject) {
|
||||
break;
|
||||
} else {
|
||||
console.log("Waiting to close the db");
|
||||
}
|
||||
}
|
||||
console.log("SQLite closed");
|
||||
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* One backup one time in this process.
|
||||
* Reset this.backupPath if you want to backup again
|
||||
* @param version
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
console.info("Backup the db");
|
||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, this.backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
this.backupShmPath = shmPath + ".bak" + version;
|
||||
fs.copyFileSync(shmPath, this.backupShmPath);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
this.backupWalPath = walPath + ".bak" + version;
|
||||
fs.copyFileSync(walPath, this.backupWalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
console.error("Patch db failed!!! Restoring the backup");
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
const walPath = Database.path + "-wal";
|
||||
|
||||
// Delete patch failed db
|
||||
try {
|
||||
if (fs.existsSync(Database.path)) {
|
||||
fs.unlinkSync(Database.path);
|
||||
}
|
||||
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.unlinkSync(shmPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Restore failed, you may need to restore the backup manually");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Restore backup
|
||||
fs.copyFileSync(this.backupPath, Database.path);
|
||||
|
||||
if (this.backupShmPath) {
|
||||
fs.copyFileSync(this.backupShmPath, shmPath);
|
||||
}
|
||||
|
||||
if (this.backupWalPath) {
|
||||
fs.copyFileSync(this.backupWalPath, walPath);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("Nothing to restore");
|
||||
}
|
||||
console.log("Stopped database");
|
||||
}
|
||||
}
|
||||
|
||||
|
57
server/image-data-uri.js
Normal file
57
server/image-data-uri.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
||||
Modified with 0 dependencies
|
||||
*/
|
||||
let fs = require("fs");
|
||||
|
||||
let ImageDataURI = (() => {
|
||||
|
||||
function decode(dataURI) {
|
||||
if (!/data:image\//.test(dataURI)) {
|
||||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)");
|
||||
return {
|
||||
imageType: regExMatches[1],
|
||||
dataBase64: regExMatches[2],
|
||||
dataBuffer: new Buffer(regExMatches[2], "base64")
|
||||
};
|
||||
}
|
||||
|
||||
function encode(data, mediaType) {
|
||||
if (!data || !mediaType) {
|
||||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
||||
return null;
|
||||
}
|
||||
|
||||
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType;
|
||||
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64");
|
||||
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64;
|
||||
|
||||
return dataImgBase64;
|
||||
}
|
||||
|
||||
function outputFile(dataURI, filePath) {
|
||||
filePath = filePath || "./";
|
||||
return new Promise((resolve, reject) => {
|
||||
let imageDecoded = decode(dataURI);
|
||||
|
||||
fs.writeFile(filePath, imageDecoded.dataBuffer, err => {
|
||||
if (err) {
|
||||
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4));
|
||||
}
|
||||
resolve(filePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
outputFile: outputFile,
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = ImageDataURI;
|
34
server/model/group.js
Normal file
34
server/model/group.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class Group extends BeanModel {
|
||||
|
||||
async toPublicJSON() {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON());
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
weight: this.weight,
|
||||
monitorList,
|
||||
};
|
||||
}
|
||||
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
SELECT monitor.* FROM monitor, monitor_group
|
||||
WHERE monitor.id = monitor_group.monitor_id
|
||||
AND group_id = ?
|
||||
ORDER BY monitor_group.weight
|
||||
`, [
|
||||
this.id,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Group;
|
@@ -1,8 +1,8 @@
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const utc = require("dayjs/plugin/utc");
|
||||
let timezone = require("dayjs/plugin/timezone");
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
/**
|
||||
@@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
*/
|
||||
class Heartbeat extends BeanModel {
|
||||
|
||||
toPublicJSON() {
|
||||
return {
|
||||
status: this.status,
|
||||
time: this.time,
|
||||
msg: "", // Hide for public
|
||||
ping: this.ping,
|
||||
};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
monitorID: this.monitor_id,
|
||||
|
18
server/model/incident.js
Normal file
18
server/model/incident.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Incident extends BeanModel {
|
||||
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
style: this.style,
|
||||
title: this.title,
|
||||
content: this.content,
|
||||
pin: this.pin,
|
||||
createdDate: this.createdDate,
|
||||
lastUpdatedDate: this.lastUpdatedDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Incident;
|
@@ -1,16 +1,16 @@
|
||||
const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require("dayjs/plugin/utc")
|
||||
let timezone = require("dayjs/plugin/timezone")
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const utc = require("dayjs/plugin/utc");
|
||||
let timezone = require("dayjs/plugin/timezone");
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification")
|
||||
const { Notification } = require("../notification");
|
||||
const version = require("../../package.json").version;
|
||||
|
||||
/**
|
||||
@@ -20,18 +20,35 @@ const version = require("../../package.json").version;
|
||||
* 2 = PENDING
|
||||
*/
|
||||
class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a object that ready to parse to JSON
|
||||
*/
|
||||
async toJSON() {
|
||||
|
||||
let notificationIDList = {};
|
||||
|
||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||
this.id,
|
||||
])
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
notificationIDList[bean.notification_id] = true;
|
||||
}
|
||||
|
||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
@@ -43,6 +60,7 @@ class Monitor extends BeanModel {
|
||||
active: this.active,
|
||||
type: this.type,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
keyword: this.keyword,
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
@@ -52,6 +70,7 @@ class Monitor extends BeanModel {
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,7 +79,7 @@ class Monitor extends BeanModel {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIgnoreTls() {
|
||||
return Boolean(this.ignoreTls)
|
||||
return Boolean(this.ignoreTls);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +109,12 @@ class Monitor extends BeanModel {
|
||||
if (! previousBeat) {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
this.id,
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
const isFirstBeat = !previousBeat;
|
||||
|
||||
let bean = R.dispense("heartbeat")
|
||||
let bean = R.dispense("heartbeat");
|
||||
bean.monitor_id = this.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = DOWN;
|
||||
@@ -131,7 +150,7 @@ class Monitor extends BeanModel {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
});
|
||||
bean.msg = `${res.status} - ${res.statusText}`
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
@@ -141,12 +160,12 @@ class Monitor extends BeanModel {
|
||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
console.error(e.message)
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
@@ -156,26 +175,26 @@ class Monitor extends BeanModel {
|
||||
|
||||
// Convert to string for object/array
|
||||
if (typeof data !== "string") {
|
||||
data = JSON.stringify(data)
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found"
|
||||
bean.msg += ", keyword is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but keyword is not found")
|
||||
throw new Error(bean.msg + ", but keyword is not found");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
bean.ping = await tcping(this.hostname, this.port);
|
||||
bean.msg = ""
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.msg = ""
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
} else if (this.type === "dns") {
|
||||
let startTime = dayjs().valueOf();
|
||||
@@ -195,7 +214,7 @@ class Monitor extends BeanModel {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2)
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
} else if (this.dns_resolve_type == "NS") {
|
||||
dnsMessage += "Servers: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
@@ -205,7 +224,7 @@ class Monitor extends BeanModel {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2)
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
}
|
||||
|
||||
if (this.dnsLastResult !== dnsMessage) {
|
||||
@@ -268,20 +287,20 @@ class Monitor extends BeanModel {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
])
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up"
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down"
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
@@ -293,30 +312,39 @@ class Monitor extends BeanModel {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else if (bean.status === PENDING) {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
|
||||
if (this.retryInterval > 0) {
|
||||
beatInterval = this.retryInterval;
|
||||
}
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
}
|
||||
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
Monitor.sendStats(io, this.id, this.user_id)
|
||||
Monitor.sendStats(io, this.id, this.user_id);
|
||||
|
||||
await R.store(bean);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
}
|
||||
if (! this.isStop) {
|
||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
beat();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,64 +430,74 @@ class Monitor extends BeanModel {
|
||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
static async calcUptime(duration, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let sec = duration * 3600;
|
||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
||||
|
||||
let heartbeatList = await R.getAll(`
|
||||
SELECT duration, time, status
|
||||
// Handle if heartbeat duration longer than the target duration
|
||||
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
|
||||
let result = await R.getRow(`
|
||||
SELECT
|
||||
-- SUM all duration, also trim off the beat out of time window
|
||||
SUM(
|
||||
CASE
|
||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||
ELSE duration
|
||||
END
|
||||
) AS total_duration,
|
||||
|
||||
-- SUM all uptime duration, also trim off the beat out of time window
|
||||
SUM(
|
||||
CASE
|
||||
WHEN (status = 1)
|
||||
THEN
|
||||
CASE
|
||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||
ELSE duration
|
||||
END
|
||||
END
|
||||
) AS uptime_duration
|
||||
FROM heartbeat
|
||||
WHERE time > DATETIME('now', ? || ' hours')
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
WHERE time > ?
|
||||
AND monitor_id = ?
|
||||
`, [
|
||||
startTime, startTime, startTime, startTime, startTime,
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
||||
|
||||
let downtime = 0;
|
||||
let total = 0;
|
||||
let uptime;
|
||||
let totalDuration = result.total_duration;
|
||||
let uptimeDuration = result.uptime_duration;
|
||||
let uptime = 0;
|
||||
|
||||
// Special handle for the first heartbeat only
|
||||
if (heartbeatList.length === 1) {
|
||||
|
||||
if (heartbeatList[0].status === 1) {
|
||||
uptime = 1;
|
||||
} else {
|
||||
if (totalDuration > 0) {
|
||||
uptime = uptimeDuration / totalDuration;
|
||||
if (uptime < 0) {
|
||||
uptime = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
for (let row of heartbeatList) {
|
||||
let value = parseInt(row.duration)
|
||||
let time = row.time
|
||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||
|
||||
// Handle if heartbeat duration longer than the target duration
|
||||
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
|
||||
if (value > sec) {
|
||||
let trim = dayjs.utc().diff(dayjs(time), "second");
|
||||
value = sec - trim;
|
||||
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
total += value;
|
||||
if (row.status === 0 || row.status === 2) {
|
||||
downtime += value;
|
||||
}
|
||||
}
|
||||
|
||||
uptime = (total - downtime) / total;
|
||||
|
||||
if (uptime < 0) {
|
||||
uptime = 0;
|
||||
if (status === UP) {
|
||||
uptime = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return uptime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Uptime
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
}
|
||||
|
13
server/model/tag.js
Normal file
13
server/model/tag.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Tag extends BeanModel {
|
||||
toJSON() {
|
||||
return {
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
color: this._color,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tag;
|
749
server/modules/apicache/apicache.js
Normal file
749
server/modules/apicache/apicache.js
Normal file
@@ -0,0 +1,749 @@
|
||||
let url = require("url");
|
||||
let MemoryCache = require("./memory-cache");
|
||||
|
||||
let t = {
|
||||
ms: 1,
|
||||
second: 1000,
|
||||
minute: 60000,
|
||||
hour: 3600000,
|
||||
day: 3600000 * 24,
|
||||
week: 3600000 * 24 * 7,
|
||||
month: 3600000 * 24 * 30,
|
||||
};
|
||||
|
||||
let instances = [];
|
||||
|
||||
let matches = function (a) {
|
||||
return function (b) {
|
||||
return a === b;
|
||||
};
|
||||
};
|
||||
|
||||
let doesntMatch = function (a) {
|
||||
return function (b) {
|
||||
return !matches(a)(b);
|
||||
};
|
||||
};
|
||||
|
||||
let logDuration = function (d, prefix) {
|
||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||
};
|
||||
|
||||
function getSafeHeaders(res) {
|
||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||
}
|
||||
|
||||
function ApiCache() {
|
||||
let memCache = new MemoryCache();
|
||||
|
||||
let globalOptions = {
|
||||
debug: false,
|
||||
defaultDuration: 3600000,
|
||||
enabled: true,
|
||||
appendKey: [],
|
||||
jsonp: false,
|
||||
redisClient: false,
|
||||
headerBlacklist: [],
|
||||
statusCodes: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
events: {
|
||||
expire: undefined,
|
||||
},
|
||||
headers: {
|
||||
// 'cache-control': 'no-cache' // example of header overwrite
|
||||
},
|
||||
trackPerformance: false,
|
||||
respectCacheControl: false,
|
||||
};
|
||||
|
||||
let middlewareOptions = [];
|
||||
let instance = this;
|
||||
let index = null;
|
||||
let timers = {};
|
||||
let performanceArray = []; // for tracking cache hit rate
|
||||
|
||||
instances.push(this);
|
||||
this.id = instances.length;
|
||||
|
||||
function debug(a, b, c, d) {
|
||||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
||||
return arg !== undefined;
|
||||
});
|
||||
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
|
||||
|
||||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
||||
}
|
||||
|
||||
function shouldCacheResponse(request, response, toggle) {
|
||||
let opt = globalOptions;
|
||||
let codes = opt.statusCodes;
|
||||
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toggle && !toggle(request, response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function addIndexEntries(key, req) {
|
||||
let groupName = req.apicacheGroup;
|
||||
|
||||
if (groupName) {
|
||||
debug("group detected \"" + groupName + "\"");
|
||||
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
||||
group.unshift(key);
|
||||
}
|
||||
|
||||
index.all.unshift(key);
|
||||
}
|
||||
|
||||
function filterBlacklistedHeaders(headers) {
|
||||
return Object.keys(headers)
|
||||
.filter(function (key) {
|
||||
return globalOptions.headerBlacklist.indexOf(key) === -1;
|
||||
})
|
||||
.reduce(function (acc, header) {
|
||||
acc[header] = headers[header];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function createCacheObject(status, headers, data, encoding) {
|
||||
return {
|
||||
status: status,
|
||||
headers: filterBlacklistedHeaders(headers),
|
||||
data: data,
|
||||
encoding: encoding,
|
||||
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
||||
};
|
||||
}
|
||||
|
||||
function cacheResponse(key, value, duration) {
|
||||
let redis = globalOptions.redisClient;
|
||||
let expireCallback = globalOptions.events.expire;
|
||||
|
||||
if (redis && redis.connected) {
|
||||
try {
|
||||
redis.hset(key, "response", JSON.stringify(value));
|
||||
redis.hset(key, "duration", duration);
|
||||
redis.expire(key, duration / 1000, expireCallback || function () {});
|
||||
} catch (err) {
|
||||
debug("[apicache] error in redis.hset()");
|
||||
}
|
||||
} else {
|
||||
memCache.add(key, value, duration, expireCallback);
|
||||
}
|
||||
|
||||
// add automatic cache clearing from duration, includes max limit on setTimeout
|
||||
timers[key] = setTimeout(function () {
|
||||
instance.clear(key, true);
|
||||
}, Math.min(duration, 2147483647));
|
||||
}
|
||||
|
||||
function accumulateContent(res, content) {
|
||||
if (content) {
|
||||
if (typeof content == "string") {
|
||||
res._apicache.content = (res._apicache.content || "") + content;
|
||||
} else if (Buffer.isBuffer(content)) {
|
||||
let oldContent = res._apicache.content;
|
||||
|
||||
if (typeof oldContent === "string") {
|
||||
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
|
||||
}
|
||||
|
||||
if (!oldContent) {
|
||||
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
|
||||
}
|
||||
|
||||
res._apicache.content = Buffer.concat(
|
||||
[oldContent, content],
|
||||
oldContent.length + content.length
|
||||
);
|
||||
} else {
|
||||
res._apicache.content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||
// monkeypatch res.end to create cache object
|
||||
res._apicache = {
|
||||
write: res.write,
|
||||
writeHead: res.writeHead,
|
||||
end: res.end,
|
||||
cacheable: true,
|
||||
content: undefined,
|
||||
};
|
||||
|
||||
// append header overwrites if applicable
|
||||
Object.keys(globalOptions.headers).forEach(function (name) {
|
||||
res.setHeader(name, globalOptions.headers[name]);
|
||||
});
|
||||
|
||||
res.writeHead = function () {
|
||||
// add cache control headers
|
||||
if (!globalOptions.headers["cache-control"]) {
|
||||
if (shouldCacheResponse(req, res, toggle)) {
|
||||
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
|
||||
} else {
|
||||
res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
|
||||
}
|
||||
}
|
||||
|
||||
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
|
||||
return res._apicache.writeHead.apply(this, arguments);
|
||||
};
|
||||
|
||||
// patch res.write
|
||||
res.write = function (content) {
|
||||
accumulateContent(res, content);
|
||||
return res._apicache.write.apply(this, arguments);
|
||||
};
|
||||
|
||||
// patch res.end
|
||||
res.end = function (content, encoding) {
|
||||
if (shouldCacheResponse(req, res, toggle)) {
|
||||
accumulateContent(res, content);
|
||||
|
||||
if (res._apicache.cacheable && res._apicache.content) {
|
||||
addIndexEntries(key, req);
|
||||
let headers = res._apicache.headers || getSafeHeaders(res);
|
||||
let cacheObject = createCacheObject(
|
||||
res.statusCode,
|
||||
headers,
|
||||
res._apicache.content,
|
||||
encoding
|
||||
);
|
||||
cacheResponse(key, cacheObject, duration);
|
||||
|
||||
// display log entry
|
||||
let elapsed = new Date() - req.apicacheTimer;
|
||||
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
|
||||
debug("_apicache.headers: ", res._apicache.headers);
|
||||
debug("res.getHeaders(): ", getSafeHeaders(res));
|
||||
debug("cacheObject: ", cacheObject);
|
||||
}
|
||||
}
|
||||
|
||||
return res._apicache.end.apply(this, arguments);
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||
if (toggle && !toggle(request, response)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let headers = getSafeHeaders(response);
|
||||
|
||||
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
||||
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
||||
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
|
||||
|
||||
// only embed apicache headers when not in production environment
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
Object.assign(headers, {
|
||||
"apicache-store": globalOptions.redisClient ? "redis" : "memory",
|
||||
"apicache-version": "1.6.2-modified",
|
||||
});
|
||||
}
|
||||
|
||||
// unstringify buffers
|
||||
let data = cacheObject.data;
|
||||
if (data && data.type === "Buffer") {
|
||||
data =
|
||||
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
|
||||
}
|
||||
|
||||
// test Etag against If-None-Match for 304
|
||||
let cachedEtag = cacheObject.headers.etag;
|
||||
let requestEtag = request.headers["if-none-match"];
|
||||
|
||||
if (requestEtag && cachedEtag === requestEtag) {
|
||||
response.writeHead(304, headers);
|
||||
return response.end();
|
||||
}
|
||||
|
||||
response.writeHead(cacheObject.status || 200, headers);
|
||||
|
||||
return response.end(data, cacheObject.encoding);
|
||||
}
|
||||
|
||||
function syncOptions() {
|
||||
for (let i in middlewareOptions) {
|
||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.clear = function (target, isAutomatic) {
|
||||
let group = index.groups[target];
|
||||
let redis = globalOptions.redisClient;
|
||||
|
||||
if (group) {
|
||||
debug("clearing group \"" + target + "\"");
|
||||
|
||||
group.forEach(function (key) {
|
||||
debug("clearing cached entry for \"" + key + "\"");
|
||||
clearTimeout(timers[key]);
|
||||
delete timers[key];
|
||||
if (!globalOptions.redisClient) {
|
||||
memCache.delete(key);
|
||||
} else {
|
||||
try {
|
||||
redis.del(key);
|
||||
} catch (err) {
|
||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||
}
|
||||
}
|
||||
index.all = index.all.filter(doesntMatch(key));
|
||||
});
|
||||
|
||||
delete index.groups[target];
|
||||
} else if (target) {
|
||||
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
|
||||
clearTimeout(timers[target]);
|
||||
delete timers[target];
|
||||
// clear actual cached entry
|
||||
if (!redis) {
|
||||
memCache.delete(target);
|
||||
} else {
|
||||
try {
|
||||
redis.del(target);
|
||||
} catch (err) {
|
||||
console.log("[apicache] error in redis.del(\"" + target + "\")");
|
||||
}
|
||||
}
|
||||
|
||||
// remove from global index
|
||||
index.all = index.all.filter(doesntMatch(target));
|
||||
|
||||
// remove target from each group that it may exist in
|
||||
Object.keys(index.groups).forEach(function (groupName) {
|
||||
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
|
||||
|
||||
// delete group if now empty
|
||||
if (!index.groups[groupName].length) {
|
||||
delete index.groups[groupName];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug("clearing entire index");
|
||||
|
||||
if (!redis) {
|
||||
memCache.clear();
|
||||
} else {
|
||||
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
||||
index.all.forEach(function (key) {
|
||||
clearTimeout(timers[key]);
|
||||
delete timers[key];
|
||||
try {
|
||||
redis.del(key);
|
||||
} catch (err) {
|
||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||
}
|
||||
});
|
||||
}
|
||||
this.resetIndex();
|
||||
}
|
||||
|
||||
return this.getIndex();
|
||||
};
|
||||
|
||||
function parseDuration(duration, defaultDuration) {
|
||||
if (typeof duration === "number") {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (typeof duration === "string") {
|
||||
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
|
||||
|
||||
if (split.length === 3) {
|
||||
let len = parseFloat(split[1]);
|
||||
let unit = split[2].replace(/s$/i, "").toLowerCase();
|
||||
if (unit === "m") {
|
||||
unit = "ms";
|
||||
}
|
||||
|
||||
return (len || 1) * (t[unit] || 0);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultDuration;
|
||||
}
|
||||
|
||||
this.getDuration = function (duration) {
|
||||
return parseDuration(duration, globalOptions.defaultDuration);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
||||
* <code>
|
||||
* app.get('/api/cache/performance', (req, res) => {
|
||||
* res.json(apicache.getPerformance())
|
||||
* })
|
||||
* </code>
|
||||
*/
|
||||
this.getPerformance = function () {
|
||||
return performanceArray.map(function (p) {
|
||||
return p.report();
|
||||
});
|
||||
};
|
||||
|
||||
this.getIndex = function (group) {
|
||||
if (group) {
|
||||
return index.groups[group];
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
};
|
||||
|
||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||
let duration = instance.getDuration(strDuration);
|
||||
let opt = {};
|
||||
|
||||
middlewareOptions.push({
|
||||
options: opt,
|
||||
});
|
||||
|
||||
let options = function (localOptions) {
|
||||
if (localOptions) {
|
||||
middlewareOptions.find(function (middleware) {
|
||||
return middleware.options === opt;
|
||||
}).localOptions = localOptions;
|
||||
}
|
||||
|
||||
syncOptions();
|
||||
|
||||
return opt;
|
||||
};
|
||||
|
||||
options(localOptions);
|
||||
|
||||
/**
|
||||
* A Function for non tracking performance
|
||||
*/
|
||||
function NOOPCachePerformance() {
|
||||
this.report = this.hit = this.miss = function () {}; // noop;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
||||
*/
|
||||
function CachePerformance() {
|
||||
/**
|
||||
* Tracks the hit rate for the last 100 requests.
|
||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 1000 requests.
|
||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 10000 requests.
|
||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 100000 requests.
|
||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* The number of calls that have passed through the middleware since the server started.
|
||||
*/
|
||||
this.callCount = 0;
|
||||
|
||||
/**
|
||||
* The total number of hits since the server started
|
||||
*/
|
||||
this.hitCount = 0;
|
||||
|
||||
/**
|
||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheHit = null;
|
||||
|
||||
/**
|
||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheMiss = null;
|
||||
|
||||
/**
|
||||
* Return performance statistics
|
||||
*/
|
||||
this.report = function () {
|
||||
return {
|
||||
lastCacheHit: this.lastCacheHit,
|
||||
lastCacheMiss: this.lastCacheMiss,
|
||||
callCount: this.callCount,
|
||||
hitCount: this.hitCount,
|
||||
missCount: this.callCount - this.hitCount,
|
||||
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
|
||||
hitRateLast100: this.hitRate(this.hitsLast100),
|
||||
hitRateLast1000: this.hitRate(this.hitsLast1000),
|
||||
hitRateLast10000: this.hitRate(this.hitsLast10000),
|
||||
hitRateLast100000: this.hitRate(this.hitsLast100000),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes a cache hit rate from an array of hits and misses.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
||||
*/
|
||||
this.hitRate = function (array) {
|
||||
let hits = 0;
|
||||
let misses = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let n8 = array[i];
|
||||
for (let j = 0; j < 4; j++) {
|
||||
switch (n8 & 3) {
|
||||
case 1:
|
||||
hits++;
|
||||
break;
|
||||
case 2:
|
||||
misses++;
|
||||
break;
|
||||
}
|
||||
n8 >>= 2;
|
||||
}
|
||||
}
|
||||
let total = hits + misses;
|
||||
if (total == 0) {
|
||||
return null;
|
||||
}
|
||||
return hits / total;
|
||||
};
|
||||
|
||||
/**
|
||||
* Record a hit or miss in the given array. It will be recorded at a position determined
|
||||
* by the current value of the callCount variable.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @param {boolean} hit true for a hit, false for a miss
|
||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
||||
* Each hit or miss is encoded as to bits as follows:
|
||||
* 00 means no hit or miss has been recorded in these bits
|
||||
* 01 encodes a hit
|
||||
* 10 encodes a miss
|
||||
*/
|
||||
this.recordHitInArray = function (array, hit) {
|
||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||
let clearMask = ~(3 << bitOffset);
|
||||
let record = (hit ? 1 : 2) << bitOffset;
|
||||
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
||||
* @param {boolean} hit true records a hit, false records a miss
|
||||
*/
|
||||
this.recordHit = function (hit) {
|
||||
this.recordHitInArray(this.hitsLast100, hit);
|
||||
this.recordHitInArray(this.hitsLast1000, hit);
|
||||
this.recordHitInArray(this.hitsLast10000, hit);
|
||||
this.recordHitInArray(this.hitsLast100000, hit);
|
||||
if (hit) {
|
||||
this.hitCount++;
|
||||
}
|
||||
this.callCount++;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a hit event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache hit
|
||||
*/
|
||||
this.hit = function (key) {
|
||||
this.recordHit(true);
|
||||
this.lastCacheHit = key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a miss event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache miss
|
||||
*/
|
||||
this.miss = function (key) {
|
||||
this.recordHit(false);
|
||||
this.lastCacheMiss = key;
|
||||
};
|
||||
}
|
||||
|
||||
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
|
||||
|
||||
performanceArray.push(perf);
|
||||
|
||||
let cache = function (req, res, next) {
|
||||
function bypass() {
|
||||
debug("bypass detected, skipping cache.");
|
||||
return next();
|
||||
}
|
||||
|
||||
// initial bypass chances
|
||||
if (!opt.enabled) {
|
||||
return bypass();
|
||||
}
|
||||
if (
|
||||
req.headers["x-apicache-bypass"] ||
|
||||
req.headers["x-apicache-force-fetch"] ||
|
||||
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
|
||||
) {
|
||||
return bypass();
|
||||
}
|
||||
|
||||
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
||||
// if (typeof middlewareToggle === 'function') {
|
||||
// if (!middlewareToggle(req, res)) return bypass()
|
||||
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
||||
// return bypass()
|
||||
// }
|
||||
|
||||
// embed timer
|
||||
req.apicacheTimer = new Date();
|
||||
|
||||
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
||||
let key = req.originalUrl || req.url;
|
||||
|
||||
// Remove querystring from key if jsonp option is enabled
|
||||
if (opt.jsonp) {
|
||||
key = url.parse(key).pathname;
|
||||
}
|
||||
|
||||
// add appendKey (either custom function or response path)
|
||||
if (typeof opt.appendKey === "function") {
|
||||
key += "$$appendKey=" + opt.appendKey(req, res);
|
||||
} else if (opt.appendKey.length > 0) {
|
||||
let appendKey = req;
|
||||
|
||||
for (let i = 0; i < opt.appendKey.length; i++) {
|
||||
appendKey = appendKey[opt.appendKey[i]];
|
||||
}
|
||||
key += "$$appendKey=" + appendKey;
|
||||
}
|
||||
|
||||
// attempt cache hit
|
||||
let redis = opt.redisClient;
|
||||
let cached = !redis ? memCache.getValue(key) : null;
|
||||
|
||||
// send if cache hit from memory-cache
|
||||
if (cached) {
|
||||
let elapsed = new Date() - req.apicacheTimer;
|
||||
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
|
||||
|
||||
perf.hit(key);
|
||||
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
||||
}
|
||||
|
||||
// send if cache hit from redis
|
||||
if (redis && redis.connected) {
|
||||
try {
|
||||
redis.hgetall(key, function (err, obj) {
|
||||
if (!err && obj && obj.response) {
|
||||
let elapsed = new Date() - req.apicacheTimer;
|
||||
debug("sending cached (redis) version of", key, logDuration(elapsed));
|
||||
|
||||
perf.hit(key);
|
||||
return sendCachedResponse(
|
||||
req,
|
||||
res,
|
||||
JSON.parse(obj.response),
|
||||
middlewareToggle,
|
||||
next,
|
||||
duration
|
||||
);
|
||||
} else {
|
||||
perf.miss(key);
|
||||
return makeResponseCacheable(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
key,
|
||||
duration,
|
||||
strDuration,
|
||||
middlewareToggle
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// bypass redis on error
|
||||
perf.miss(key);
|
||||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||
}
|
||||
} else {
|
||||
perf.miss(key);
|
||||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||
}
|
||||
};
|
||||
|
||||
cache.options = options;
|
||||
|
||||
return cache;
|
||||
};
|
||||
|
||||
this.options = function (options) {
|
||||
if (options) {
|
||||
Object.assign(globalOptions, options);
|
||||
syncOptions();
|
||||
|
||||
if ("defaultDuration" in options) {
|
||||
// Convert the default duration to a number in milliseconds (if needed)
|
||||
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
|
||||
}
|
||||
|
||||
if (globalOptions.trackPerformance) {
|
||||
debug("WARNING: using trackPerformance flag can cause high memory usage!");
|
||||
}
|
||||
|
||||
return this;
|
||||
} else {
|
||||
return globalOptions;
|
||||
}
|
||||
};
|
||||
|
||||
this.resetIndex = function () {
|
||||
index = {
|
||||
all: [],
|
||||
groups: {},
|
||||
};
|
||||
};
|
||||
|
||||
this.newInstance = function (config) {
|
||||
let instance = new ApiCache();
|
||||
|
||||
if (config) {
|
||||
instance.options(config);
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
this.clone = function () {
|
||||
return this.newInstance(this.options());
|
||||
};
|
||||
|
||||
// initialize index
|
||||
this.resetIndex();
|
||||
}
|
||||
|
||||
module.exports = new ApiCache();
|
14
server/modules/apicache/index.js
Normal file
14
server/modules/apicache/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const apicache = require("./apicache");
|
||||
|
||||
apicache.options({
|
||||
headerBlacklist: [
|
||||
"cache-control"
|
||||
],
|
||||
headers: {
|
||||
// Disable client side cache, only server side cache.
|
||||
// BUG! Not working for the second request
|
||||
"cache-control": "no-cache",
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = apicache;
|
59
server/modules/apicache/memory-cache.js
Normal file
59
server/modules/apicache/memory-cache.js
Normal file
@@ -0,0 +1,59 @@
|
||||
function MemoryCache() {
|
||||
this.cache = {};
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||
let old = this.cache[key];
|
||||
let instance = this;
|
||||
|
||||
let entry = {
|
||||
value: value,
|
||||
expire: time + Date.now(),
|
||||
timeout: setTimeout(function () {
|
||||
instance.delete(key);
|
||||
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key);
|
||||
}, time)
|
||||
};
|
||||
|
||||
this.cache[key] = entry;
|
||||
this.size = Object.keys(this.cache).length;
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
||||
MemoryCache.prototype.delete = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
if (entry) {
|
||||
clearTimeout(entry.timeout);
|
||||
}
|
||||
|
||||
delete this.cache[key];
|
||||
|
||||
this.size = Object.keys(this.cache).length;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
MemoryCache.prototype.get = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
||||
MemoryCache.prototype.getValue = function (key) {
|
||||
let entry = this.get(key);
|
||||
|
||||
return entry && entry.value;
|
||||
};
|
||||
|
||||
MemoryCache.prototype.clear = function () {
|
||||
Object.keys(this.cache).forEach(function (key) {
|
||||
this.delete(key);
|
||||
}, this);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = MemoryCache;
|
26
server/notification-providers/apprise.js
Normal file
26
server/notification-providers/apprise.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const child_process = require("child_process");
|
||||
|
||||
class Apprise extends NotificationProvider {
|
||||
|
||||
name = "apprise";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
}
|
||||
|
||||
throw new Error(output)
|
||||
} else {
|
||||
return "No output from apprise";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Apprise;
|
115
server/notification-providers/discord.js
Normal file
115
server/notification-providers/discord.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Discord extends NotificationProvider {
|
||||
|
||||
name = "discord";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let discordtestdata = {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
let discorddowndata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
|
||||
color: 16711680,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
let discordupdata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
|
||||
color: 65280,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Discord;
|
28
server/notification-providers/gotify.js
Normal file
28
server/notification-providers/gotify.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gotify extends NotificationProvider {
|
||||
|
||||
name = "gotify";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
}
|
||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
})
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Gotify;
|
60
server/notification-providers/line.js
Normal file
60
server/notification-providers/line.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Line extends NotificationProvider {
|
||||
|
||||
name = "line";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.lineChannelAccessToken
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, testMessage, config)
|
||||
} else if (heartbeatJSON["status"] == DOWN) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, downMessage, config)
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, upMessage, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Line;
|
48
server/notification-providers/lunasea.js
Normal file
48
server/notification-providers/lunasea.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LunaSea extends NotificationProvider {
|
||||
|
||||
name = "lunasea";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(lunaseadevice, testdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, downdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == UP) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, updata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LunaSea;
|
123
server/notification-providers/mattermost.js
Normal file
123
server/notification-providers/mattermost.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Mattermost extends NotificationProvider {
|
||||
|
||||
name = "mattermost";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
}
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel;
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Mattermost;
|
36
server/notification-providers/notification-provider.js
Normal file
36
server/notification-providers/notification-provider.js
Normal file
@@ -0,0 +1,36 @@
|
||||
class NotificationProvider {
|
||||
|
||||
/**
|
||||
* Notification Provider Name
|
||||
* @type string
|
||||
*/
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Return Successful Message
|
||||
* Throw Error with fail msg
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
throw new Error("Have to override Notification.send(...)");
|
||||
}
|
||||
|
||||
throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotificationProvider;
|
40
server/notification-providers/octopush.js
Normal file
40
server/notification-providers/octopush.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Octopush extends NotificationProvider {
|
||||
|
||||
name = "octopush";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [
|
||||
{
|
||||
"phone_number": notification.octopushPhoneNumber
|
||||
}
|
||||
],
|
||||
//octopush not supporting non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": notification.octopushSMSType,
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Octopush;
|
50
server/notification-providers/pushbullet.js
Normal file
50
server/notification-providers/pushbullet.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Pushbullet extends NotificationProvider {
|
||||
|
||||
name = "pushbullet";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"type": "note",
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(pushbulletUrl, testdata, config)
|
||||
} else if (heartbeatJSON["status"] == DOWN) {
|
||||
let downdata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, downdata, config)
|
||||
} else if (heartbeatJSON["status"] == UP) {
|
||||
let updata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, updata, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Pushbullet;
|
49
server/notification-providers/pushover.js
Normal file
49
server/notification-providers/pushover.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Pushover extends NotificationProvider {
|
||||
|
||||
name = "pushover";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Pushover;
|
30
server/notification-providers/pushy.js
Normal file
30
server/notification-providers/pushy.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Pushy extends NotificationProvider {
|
||||
|
||||
name = "pushy";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
"to": notification.pushyToken,
|
||||
"data": {
|
||||
"message": "Uptime-Kuma"
|
||||
},
|
||||
"notification": {
|
||||
"body": msg,
|
||||
"badge": 1,
|
||||
"sound": "ping.aiff"
|
||||
}
|
||||
})
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Pushy;
|
46
server/notification-providers/rocket-chat.js
Normal file
46
server/notification-providers/rocket-chat.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
name = "rocket.chat";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Rocket.chat testing successful.",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title_link": notification.rocketbutton,
|
||||
"text": "*Message*\n" + msg,
|
||||
"color": "#32cd32"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RocketChat;
|
27
server/notification-providers/signal.js
Normal file
27
server/notification-providers/signal.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Signal extends NotificationProvider {
|
||||
|
||||
name = "signal";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"number": notification.signalNumber,
|
||||
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Signal;
|
70
server/notification-providers/slack.js
Normal file
70
server/notification-providers/slack.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
|
||||
name = "slack";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Slack testing successful.",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"blocks": [{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
}],
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Slack;
|
48
server/notification-providers/smtp.js
Normal file
48
server/notification-providers/smtp.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
name = "smtp";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
};
|
||||
|
||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||
if (notification.smtpUsername || notification.smtpPassword) {
|
||||
config.auth = {
|
||||
user: notification.smtpUsername,
|
||||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
let bodyTextContent = msg;
|
||||
if (heartbeatJSON) {
|
||||
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
from: notification.smtpFrom,
|
||||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
},
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMTP;
|
124
server/notification-providers/teams.js
Normal file
124
server/notification-providers/teams.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Teams extends NotificationProvider {
|
||||
name = "teams";
|
||||
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
if (status === DOWN) {
|
||||
return `🔴 Application [${monitorName}] went down`;
|
||||
} else if (status === UP) {
|
||||
return `✅ Application [${monitorName}] is back online`;
|
||||
}
|
||||
return "Notification";
|
||||
};
|
||||
|
||||
_getThemeColor = (status) => {
|
||||
if (status === DOWN) {
|
||||
return "ff0000";
|
||||
}
|
||||
if (status === UP) {
|
||||
return "00e804";
|
||||
}
|
||||
return "008cff";
|
||||
};
|
||||
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
monitorMessage,
|
||||
monitorName,
|
||||
monitorUrl,
|
||||
}) => {
|
||||
const notificationMessage = this._statusMessageFactory(
|
||||
status,
|
||||
monitorName
|
||||
);
|
||||
|
||||
const facts = [];
|
||||
|
||||
if (monitorName) {
|
||||
facts.push({
|
||||
name: "Monitor",
|
||||
value: monitorName,
|
||||
});
|
||||
}
|
||||
|
||||
if (monitorUrl) {
|
||||
facts.push({
|
||||
name: "URL",
|
||||
value: monitorUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org/extensions",
|
||||
"@type": "MessageCard",
|
||||
themeColor: this._getThemeColor(status),
|
||||
summary: notificationMessage,
|
||||
sections: [
|
||||
{
|
||||
activityImage:
|
||||
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||
activityTitle: "**Uptime Kuma**",
|
||||
},
|
||||
{
|
||||
activityTitle: notificationMessage,
|
||||
},
|
||||
{
|
||||
activityTitle: "**Description**",
|
||||
text: monitorMessage,
|
||||
facts,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
_sendNotification = async (webhookUrl, payload) => {
|
||||
await axios.post(webhookUrl, payload);
|
||||
};
|
||||
|
||||
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: msg
|
||||
});
|
||||
|
||||
return this._sendNotification(webhookUrl, payload);
|
||||
};
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
await this._handleGeneralNotification(notification.webhookUrl, msg);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
}
|
||||
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: heartbeatJSON.msg,
|
||||
monitorName: monitorJSON.name,
|
||||
monitorUrl: url,
|
||||
status: heartbeatJSON.status,
|
||||
});
|
||||
|
||||
await this._sendNotification(notification.webhookUrl, payload);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Teams;
|
27
server/notification-providers/telegram.js
Normal file
27
server/notification-providers/telegram.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Telegram extends NotificationProvider {
|
||||
|
||||
name = "telegram";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
params: {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
},
|
||||
})
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Telegram;
|
44
server/notification-providers/webhook.js
Normal file
44
server/notification-providers/webhook.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const FormData = require("form-data");
|
||||
|
||||
class Webhook extends NotificationProvider {
|
||||
|
||||
name = "webhook";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let finalData;
|
||||
let config = {};
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
|
||||
config = {
|
||||
headers: finalData.getHeaders(),
|
||||
}
|
||||
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Webhook;
|
@@ -1,600 +1,77 @@
|
||||
const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const FormData = require("form-data");
|
||||
const nodemailer = require("nodemailer");
|
||||
const child_process = require("child_process");
|
||||
const Apprise = require("./notification-providers/apprise");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
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 Octopush = require("./notification-providers/octopush");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const Pushover = require("./notification-providers/pushover");
|
||||
const Pushy = require("./notification-providers/pushy");
|
||||
const RocketChat = require("./notification-providers/rocket-chat");
|
||||
const Signal = require("./notification-providers/signal");
|
||||
const Slack = require("./notification-providers/slack");
|
||||
const SMTP = require("./notification-providers/smtp");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
|
||||
class Notification {
|
||||
|
||||
providerList = {};
|
||||
|
||||
static init() {
|
||||
console.log("Prepare Notification Providers");
|
||||
|
||||
this.providerList = {};
|
||||
|
||||
const list = [
|
||||
new Apprise(),
|
||||
new Discord(),
|
||||
new Teams(),
|
||||
new Gotify(),
|
||||
new Line(),
|
||||
new LunaSea(),
|
||||
new Mattermost(),
|
||||
new Octopush(),
|
||||
new Pushbullet(),
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
new RocketChat(),
|
||||
new Signal(),
|
||||
new Slack(),
|
||||
new SMTP(),
|
||||
new Telegram(),
|
||||
new Webhook(),
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
if (! item.name) {
|
||||
throw new Error("Notification provider without name");
|
||||
}
|
||||
|
||||
if (this.providerList[item.name]) {
|
||||
throw new Error("Duplicate notification provider name");
|
||||
}
|
||||
this.providerList[item.name] = item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param notification
|
||||
* @param msg
|
||||
* @param monitorJSON
|
||||
* @param heartbeatJSON
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* Throw Error with fail msg
|
||||
*/
|
||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
if (notification.type === "telegram") {
|
||||
try {
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
params: {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
},
|
||||
})
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
} else if (notification.type === "gotify") {
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
}
|
||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma",
|
||||
})
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "webhook") {
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let finalData;
|
||||
let config = {};
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
|
||||
config = {
|
||||
headers: finalData.getHeaders(),
|
||||
}
|
||||
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "smtp") {
|
||||
return await Notification.smtp(notification, msg)
|
||||
|
||||
} else if (notification.type === "discord") {
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let discordtestdata = {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let discorddowndata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
|
||||
color: 16711680,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let discordupdata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [{
|
||||
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
|
||||
color: 65280,
|
||||
timestamp: heartbeatJSON["time"],
|
||||
fields: [
|
||||
{
|
||||
name: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "signal") {
|
||||
try {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"number": notification.signalNumber,
|
||||
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushy") {
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
"to": notification.pushyToken,
|
||||
"data": {
|
||||
"message": "Uptime-Kuma"
|
||||
},
|
||||
"notification": {
|
||||
"body": msg,
|
||||
"badge": 1,
|
||||
"sound": "ping.aiff"
|
||||
}
|
||||
})
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
} else if (notification.type === "octopush") {
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [
|
||||
{
|
||||
"phone_number": notification.octopushPhoneNumber
|
||||
}
|
||||
],
|
||||
//octopush not supporting non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": notification.octopushSMSType,
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
} else if (notification.type === "slack") {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Slack testing successful.",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"blocks": [{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
}],
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "rocket.chat") {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Rocket.chat testing successful.",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title_link": notification.rocketbutton,
|
||||
"text": "*Message*\n" + msg,
|
||||
"color": "#32cd32"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "mattermost") {
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
}
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel;
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushover") {
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||
"user": notification.pushoveruserkey,
|
||||
"token": notification.pushoverapptoken,
|
||||
"sound": notification.pushoversounds,
|
||||
"priority": notification.pushoverpriority,
|
||||
"title": notification.pushovertitle,
|
||||
"retry": "30",
|
||||
"expire": "3600",
|
||||
"html": 1,
|
||||
}
|
||||
await axios.post(pushoverlink, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "apprise") {
|
||||
|
||||
return Notification.apprise(notification, msg)
|
||||
|
||||
} else if (notification.type === "lunasea") {
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(lunaseadevice, testdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, downdata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == 1) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(lunaseadevice, updata)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushbullet") {
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"type": "note",
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": "Testing Successful.",
|
||||
}
|
||||
await axios.post(pushbulletUrl, testdata, config)
|
||||
} else if (heartbeatJSON["status"] == 0) {
|
||||
let downdata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, downdata, config)
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let updata = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||
}
|
||||
await axios.post(pushbulletUrl, updata, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
} else if (notification.type === "line") {
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.lineChannelAccessToken
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, testMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 0) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, downMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, upMessage, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
if (this.providerList[notification.type]) {
|
||||
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
|
||||
} else {
|
||||
throw new Error("Notification type is not supported")
|
||||
throw new Error("Notification type is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,8 +94,15 @@ class Notification {
|
||||
|
||||
bean.name = notification.name;
|
||||
bean.user_id = userID;
|
||||
bean.config = JSON.stringify(notification)
|
||||
bean.config = JSON.stringify(notification);
|
||||
bean.is_default = notification.isDefault || false;
|
||||
await R.store(bean)
|
||||
|
||||
if (notification.applyExisting) {
|
||||
await applyNotificationEveryMonitor(bean.id, userID);
|
||||
}
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
static async delete(notificationID, userID) {
|
||||
@@ -634,52 +118,6 @@ class Notification {
|
||||
await R.trash(bean)
|
||||
}
|
||||
|
||||
static async smtp(notification, msg) {
|
||||
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
};
|
||||
|
||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||
if (notification.smtpUsername || notification.smtpPassword) {
|
||||
config.auth = {
|
||||
user: notification.smtpUsername,
|
||||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
// send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
text: msg,
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
|
||||
static async apprise(notification, msg) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
}
|
||||
|
||||
throw new Error(output)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require("command-exists").sync;
|
||||
let exists = commandExistsSync("apprise");
|
||||
@@ -688,18 +126,24 @@ class Notification {
|
||||
|
||||
}
|
||||
|
||||
function throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
||||
userID
|
||||
]);
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
|
||||
monitors[i].id,
|
||||
notificationID,
|
||||
])
|
||||
|
||||
if (! checkNotification) {
|
||||
let relation = R.dispense("monitor_notification");
|
||||
relation.monitor_id = monitors[i].id;
|
||||
relation.notification_id = notificationID;
|
||||
await R.store(relation)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
151
server/routers/api-router.js
Normal file
151
server/routers/api-router.js
Normal file
@@ -0,0 +1,151 @@
|
||||
let express = require("express");
|
||||
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const server = require("../server");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
|
||||
router.get("/api/entry-page", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json(server.entryPage);
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
let config = await getSettings("statusPage");
|
||||
|
||||
if (! config.statusPageTheme) {
|
||||
config.statusPageTheme = "light";
|
||||
}
|
||||
|
||||
if (! config.statusPagePublished) {
|
||||
config.statusPagePublished = true;
|
||||
}
|
||||
|
||||
if (! config.title) {
|
||||
config.title = "Uptime Kuma";
|
||||
}
|
||||
|
||||
response.json(config);
|
||||
});
|
||||
|
||||
// Status Page - Get the current Incident
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/incident", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||
|
||||
if (incident) {
|
||||
incident = incident.toPublicJSON();
|
||||
}
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
incident,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page - Monitor List
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
const publicGroupList = [];
|
||||
let list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||
|
||||
for (let groupBean of list) {
|
||||
publicGroupList.push(await groupBean.toPublicJSON());
|
||||
}
|
||||
|
||||
response.json(publicGroupList);
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Polling Data
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let heartbeatList = {};
|
||||
let uptimeList = {};
|
||||
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
`);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
async function checkPublished() {
|
||||
if (! await isPublished()) {
|
||||
throw new Error("The status page is not published");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default is published
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function isPublished() {
|
||||
const value = await setting("statusPagePublished");
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function send403(res, msg = "") {
|
||||
res.status(403).json({
|
||||
"status": "fail",
|
||||
"msg": msg,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
906
server/server.js
906
server/server.js
File diff suppressed because it is too large
Load Diff
161
server/socket-handlers/status-page-socket-handler.js
Normal file
161
server/socket-handlers/status-page-socket-handler.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { checkLogin, setSettings } = require("../util-server");
|
||||
const dayjs = require("dayjs");
|
||||
const { debug } = require("../../src/util");
|
||||
const ImageDataURI = require("../image-data-uri");
|
||||
const Database = require("../database");
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
socket.on("postIncident", async (incident, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 ");
|
||||
|
||||
let incidentBean;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean = await R.findOne("incident", " id = ?", [
|
||||
incident.id
|
||||
]);
|
||||
}
|
||||
|
||||
if (incidentBean == null) {
|
||||
incidentBean = R.dispense("incident");
|
||||
}
|
||||
|
||||
incidentBean.title = incident.title;
|
||||
incidentBean.content = incident.content;
|
||||
incidentBean.style = incident.style;
|
||||
incidentBean.pin = true;
|
||||
|
||||
if (incident.id) {
|
||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||
} else {
|
||||
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
|
||||
}
|
||||
|
||||
await R.store(incidentBean);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
incident: incidentBean.toPublicJSON(),
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("unpinIncident", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Save Status Page
|
||||
// imgDataUrl Only Accept PNG!
|
||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
const header = "data:image/png;base64,";
|
||||
|
||||
// Check logo format
|
||||
// If is image data url, convert to png file
|
||||
// Else assume it is a url, nothing to do
|
||||
if (imgDataUrl.startsWith("data:")) {
|
||||
if (! imgDataUrl.startsWith(header)) {
|
||||
throw new Error("Only allowed PNG logo.");
|
||||
}
|
||||
|
||||
// Convert to file
|
||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||
|
||||
} else {
|
||||
config.icon = imgDataUrl;
|
||||
}
|
||||
|
||||
// Save Config
|
||||
await setSettings("statusPage", config);
|
||||
|
||||
// Save Public Group List
|
||||
const groupIDList = [];
|
||||
let groupOrder = 1;
|
||||
|
||||
for (let group of publicGroupList) {
|
||||
let groupBean;
|
||||
if (group.id) {
|
||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||
group.id
|
||||
]);
|
||||
} else {
|
||||
groupBean = R.dispense("group");
|
||||
}
|
||||
|
||||
groupBean.name = group.name;
|
||||
groupBean.public = true;
|
||||
groupBean.weight = groupOrder++;
|
||||
|
||||
await R.store(groupBean);
|
||||
|
||||
await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
|
||||
groupBean.id
|
||||
]);
|
||||
|
||||
let monitorOrder = 1;
|
||||
console.log(group.monitorList);
|
||||
|
||||
for (let monitor of group.monitorList) {
|
||||
let relationBean = R.dispense("monitor_group");
|
||||
relationBean.weight = monitorOrder++;
|
||||
relationBean.group_id = groupBean.id;
|
||||
relationBean.monitor_id = monitor.id;
|
||||
await R.store(relationBean);
|
||||
}
|
||||
|
||||
groupIDList.push(groupBean.id);
|
||||
group.id = groupBean.id;
|
||||
}
|
||||
|
||||
// Delete groups that not in the list
|
||||
debug("Delete groups that not in the list");
|
||||
const slots = groupIDList.map(() => "?").join(",");
|
||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
publicGroupList,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
@@ -23,7 +23,7 @@ exports.initJWTSecret = async () => {
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||
await R.store(jwtSecretBean);
|
||||
return jwtSecretBean;
|
||||
}
|
||||
};
|
||||
|
||||
exports.tcping = function (hostname, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) {
|
||||
resolve(Math.round(data.max));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.ping = async (hostname) => {
|
||||
try {
|
||||
@@ -57,7 +57,7 @@ exports.ping = async (hostname) => {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (ms === null) {
|
||||
reject(new Error(stdout))
|
||||
reject(new Error(stdout));
|
||||
} else {
|
||||
resolve(Math.round(ms))
|
||||
resolve(Math.round(ms));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
@@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.setting = async function (key) {
|
||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
@@ -108,29 +108,29 @@ exports.setting = async function (key) {
|
||||
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
debug(`Get Setting: ${key}: ${v}`)
|
||||
debug(`Get Setting: ${key}: ${v}`);
|
||||
return v;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
])
|
||||
]);
|
||||
if (!bean) {
|
||||
bean = R.dispense("setting")
|
||||
bean = R.dispense("setting");
|
||||
bean.key = key;
|
||||
}
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean)
|
||||
}
|
||||
await R.store(bean);
|
||||
};
|
||||
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
type,
|
||||
])
|
||||
]);
|
||||
|
||||
let result = {};
|
||||
|
||||
@@ -143,7 +143,7 @@ exports.getSettings = async function (type) {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
exports.setSettings = async function (type, data) {
|
||||
let keyList = Object.keys(data);
|
||||
@@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) {
|
||||
|
||||
if (bean.type === type) {
|
||||
bean.value = JSON.stringify(data[key]);
|
||||
promiseList.push(R.store(bean))
|
||||
promiseList.push(R.store(bean));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
};
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
@@ -218,7 +218,7 @@ exports.checkCertificate = function (res) {
|
||||
issuer,
|
||||
fingerprint,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the provided status code is within the accepted ranges
|
||||
// Param: status - the status code to check
|
||||
@@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.getTotalClientInRoom = (io, roomName) => {
|
||||
|
||||
@@ -270,4 +270,31 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.genSecret = () => {
|
||||
let secret = "";
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charsLength = chars.length;
|
||||
for ( let i = 0; i < 64; i++ ) {
|
||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return secret;
|
||||
};
|
||||
|
||||
exports.allowDevAllOrigin = (res) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
exports.allowAllOrigin(res);
|
||||
}
|
||||
};
|
||||
|
||||
exports.allowAllOrigin = (res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
};
|
||||
|
||||
exports.checkLogin = (socket) => {
|
||||
if (! socket.userID) {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
};
|
||||
|
@@ -144,7 +144,9 @@ h2 {
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
background-color: $dark-bg;
|
||||
&:not(.alert) {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
@@ -152,7 +154,7 @@ h2 {
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
background-color: #131a21;
|
||||
background-color: #232f3b;
|
||||
}
|
||||
|
||||
a,
|
||||
@@ -255,6 +257,18 @@ h2 {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
tbody {
|
||||
@@ -268,6 +282,16 @@ h2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
&.bg-info,
|
||||
&.bg-warning,
|
||||
&.bg-danger,
|
||||
&.bg-light {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -288,3 +312,119 @@ h2 {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-right-enter-active {
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-right-leave-active {
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-right-enter-from,
|
||||
.slide-fade-right-leave-to {
|
||||
transform: translateX(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
&.scrollbar {
|
||||
min-height: calc(100vh - 240px);
|
||||
max-height: calc(100vh - 30px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 13px 15px 10px 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #122f21;
|
||||
background-color: $primary;
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #055160;
|
||||
background-color: #cff4fc;
|
||||
border-color: #cff4fc;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #842029;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f8d7da;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: #4caf50;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
[contenteditable=true] {
|
||||
transition: all $easing-in 0.2s;
|
||||
background-color: rgba(239, 239, 239, 0.7);
|
||||
border-radius: 8px;
|
||||
|
||||
&:focus {
|
||||
outline: 0 solid #eee;
|
||||
background-color: rgba(245, 245, 245, 0.9);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(239, 239, 239, 0.8);
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background-color: rgba(239, 239, 239, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
&::after {
|
||||
margin-left: 5px;
|
||||
content: "🖊️";
|
||||
font-size: 13px;
|
||||
color: #eee;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
.action {
|
||||
transition: all $easing-in 0.2s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.vue-image-crop-upload .vicp-wrap {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
@@ -16,3 +16,5 @@ $dark-border-color: #1d2634;
|
||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
|
||||
|
||||
$dropdown-border-radius: 0.5rem;
|
||||
|
@@ -25,21 +25,32 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
heartbeatList: {
|
||||
type: Array,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
beatWidth: 10,
|
||||
beatHeight: 30,
|
||||
hoverScale: 1.5,
|
||||
beatMargin: 3, // Odd number only, even = blurry
|
||||
beatMargin: 4,
|
||||
move: false,
|
||||
maxBeat: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
/**
|
||||
* If heartbeatList is null, get it from $root.heartbeatList
|
||||
*/
|
||||
beatList() {
|
||||
return this.$root.heartbeatList[this.monitorId]
|
||||
if (this.heartbeatList === null) {
|
||||
return this.$root.heartbeatList[this.monitorId];
|
||||
} else {
|
||||
return this.heartbeatList;
|
||||
}
|
||||
},
|
||||
|
||||
shortBeatList() {
|
||||
@@ -118,15 +129,31 @@ export default {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
beforeMount() {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
if (this.heartbeatList === null) {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.size === "small") {
|
||||
this.beatWidth = 5.6;
|
||||
this.beatMargin = 2.4;
|
||||
this.beatHeight = 16
|
||||
this.beatWidth = 5;
|
||||
this.beatHeight = 16;
|
||||
this.beatMargin = 2;
|
||||
}
|
||||
|
||||
// Suddenly, have an idea how to handle it universally.
|
||||
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
|
||||
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
||||
const actualMargin = this.beatMargin * window.devicePixelRatio;
|
||||
|
||||
if (! Number.isInteger(actualWidth)) {
|
||||
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
||||
}
|
||||
|
||||
if (! Number.isInteger(actualMargin)) {
|
||||
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this.resize);
|
||||
|
78
src/components/HiddenInput.vue
Normal file
78
src/components/HiddenInput.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
ref="input"
|
||||
v-model="model"
|
||||
:type="visibility"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:autocomplete="autocomplete"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
>
|
||||
|
||||
<a v-if="visibility == 'password'" class="btn btn-outline-primary" @click="showInput()">
|
||||
<font-awesome-icon icon="eye" />
|
||||
</a>
|
||||
<a v-if="visibility == 'text'" class="btn btn-outline-primary" @click="hideInput()">
|
||||
<font-awesome-icon icon="eye-slash" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: 255
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
required: {
|
||||
type: Boolean
|
||||
},
|
||||
readonly: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visibility: "password",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
showInput() {
|
||||
this.visibility = "text";
|
||||
},
|
||||
hideInput() {
|
||||
this.visibility = "password";
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -4,16 +4,23 @@
|
||||
<form @submit.prevent="submit">
|
||||
<h1 class="h3 mb-3 fw-normal" />
|
||||
|
||||
<div class="form-floating">
|
||||
<div v-if="!tokenRequired" class="form-floating">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||
<label for="floatingInput">{{ $t("Username") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<div v-if="!tokenRequired" class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="tokenRequired">
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||
<label for="floatingToken">{{ $t("Token") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||
<div class="form-check">
|
||||
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||
@@ -42,16 +49,24 @@ export default {
|
||||
processing: false,
|
||||
username: "",
|
||||
password: "",
|
||||
|
||||
token: "",
|
||||
res: null,
|
||||
tokenRequired: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.login(this.username, this.password, (res) => {
|
||||
|
||||
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||
this.processing = false;
|
||||
this.res = res;
|
||||
console.log(res)
|
||||
|
||||
if (res.tokenRequired) {
|
||||
this.tokenRequired = true;
|
||||
} else {
|
||||
this.res = res;
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
@@ -1,44 +1,69 @@
|
||||
<template>
|
||||
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
<div class="shadow-box mb-3">
|
||||
<div class="list-header">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
<a v-if="searchText == ''" class="search-icon">
|
||||
<font-awesome-icon icon="search" />
|
||||
</a>
|
||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
scrollbar: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
@@ -68,6 +93,17 @@ export default {
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText != "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText))
|
||||
})
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
@@ -75,6 +111,9 @@ export default {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
},
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -87,57 +126,51 @@ export default {
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
&.scrollbar {
|
||||
min-height: calc(100vh - 240px);
|
||||
max-height: calc(100vh - 30px);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
}
|
||||
.list-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 10px 10px 0 0;
|
||||
margin: -10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 13px 15px 10px 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
.dark & {
|
||||
background-color: #161b22;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
padding: 10px;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.monitorItem {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tags {
|
||||
padding-left: 62px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -11,12 +11,13 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="type" v-model="notification.type" class="form-select">
|
||||
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="notification-type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="teams">Microsoft Teams</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
<option value="slack">Slack</option>
|
||||
@@ -33,48 +34,13 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||
<label for="notification-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type === 'telegram'">
|
||||
<div class="mb-3">
|
||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
</div>
|
||||
<Telegram v-if="notification.type === 'telegram'" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
|
||||
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
|
||||
Auto Get
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<template v-if="notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO: Convert all into vue components, but not an easy task. -->
|
||||
|
||||
<template v-if="notification.type === 'webhook'">
|
||||
<div class="mb-3">
|
||||
@@ -100,49 +66,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'smtp'">
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="secure">
|
||||
Secure
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Generally, true for 465, false for other ports.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
|
||||
</div>
|
||||
</template>
|
||||
<SMTP v-if="notification.type === 'smtp'" />
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
@@ -157,6 +81,11 @@
|
||||
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
|
||||
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'signal'">
|
||||
@@ -195,7 +124,7 @@
|
||||
<template v-if="notification.type === 'gotify'">
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
|
||||
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
@@ -306,13 +235,13 @@
|
||||
<template v-if="notification.type === 'pushy'">
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
|
||||
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
|
||||
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px;">
|
||||
@@ -323,7 +252,7 @@
|
||||
<template v-if="notification.type === 'octopush'">
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<input id="octopush-key" v-model="notification.octopushAPIKey" type="text" class="form-control" required>
|
||||
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
@@ -354,9 +283,9 @@
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
|
||||
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
|
||||
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
@@ -442,7 +371,7 @@
|
||||
<template v-if="notification.type === 'pushbullet'">
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">Access Token</label>
|
||||
<input id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" type="text" class="form-control" required>
|
||||
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
@@ -453,7 +382,7 @@
|
||||
<template v-if="notification.type === 'line'">
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||
<input id="line-channel-access-token" v-model="notification.lineChannelAccessToken" type="text" class="form-control" required>
|
||||
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Basic Settings</b>
|
||||
@@ -469,7 +398,31 @@
|
||||
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
|
||||
|
||||
<Teams v-if="notification.type === 'teams'" />
|
||||
|
||||
<div class="mb-3 mt-4">
|
||||
<hr class="dropdown-divider mb-4">
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="notification.isDefault" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("Default enabled") }}</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ $t("enableDefaultNotificationDescription") }}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("Apply on all existing monitors") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
{{ $t("Delete") }}
|
||||
@@ -478,6 +431,7 @@
|
||||
{{ $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>
|
||||
@@ -494,16 +448,23 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification"
|
||||
|
||||
import Confirm from "./Confirm.vue";
|
||||
const toast = useToast()
|
||||
import HiddenInput from "./HiddenInput.vue";
|
||||
import Telegram from "./notifications/Telegram.vue";
|
||||
import Teams from "./notifications/Teams.vue";
|
||||
import SMTP from "./notifications/SMTP.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
HiddenInput,
|
||||
Telegram,
|
||||
Teams,
|
||||
SMTP,
|
||||
},
|
||||
props: {},
|
||||
emits: ["added"],
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
@@ -512,22 +473,13 @@ export default {
|
||||
notification: {
|
||||
name: "",
|
||||
type: null,
|
||||
gotifyPriority: 8,
|
||||
isDefault: false,
|
||||
// Do not set default value here, please scroll to show()
|
||||
},
|
||||
appriseInstalled: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
telegramGetUpdatesURL() {
|
||||
let token = "<YOUR BOT TOKEN HERE>"
|
||||
|
||||
if (this.notification.telegramBotToken) {
|
||||
token = this.notification.telegramBotToken;
|
||||
}
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"notification.type"(to, from) {
|
||||
let oldName;
|
||||
@@ -572,11 +524,13 @@ export default {
|
||||
this.notification = {
|
||||
name: "",
|
||||
type: null,
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
// Default set to Telegram
|
||||
this.notification.type = "telegram"
|
||||
this.notification.gotifyPriority = 8
|
||||
// Set Default value here
|
||||
this.notification.type = "telegram";
|
||||
this.notification.gotifyPriority = 8;
|
||||
this.notification.smtpSecure = false;
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
@@ -589,7 +543,13 @@ export default {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide()
|
||||
this.modal.hide();
|
||||
|
||||
// Emit added event, doesn't emit edit.
|
||||
if (! this.id) {
|
||||
this.$emit("added", res.id);
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -613,32 +573,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async autoGetTelegramChatID() {
|
||||
try {
|
||||
let res = await axios.get(this.telegramGetUpdatesURL)
|
||||
|
||||
if (res.data.result.length >= 1) {
|
||||
let update = res.data.result[res.data.result.length - 1]
|
||||
|
||||
if (update.channel_post) {
|
||||
this.notification.telegramChatID = update.channel_post.chat.id;
|
||||
} else if (update.message) {
|
||||
this.notification.telegramChatID = update.message.chat.id;
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
144
src/components/PublicGroupList.vue
Normal file
144
src/components/PublicGroupList.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<!-- Group List -->
|
||||
<Draggable
|
||||
v-model="$root.publicGroupList"
|
||||
:disabled="!editMode"
|
||||
item-key="id"
|
||||
:animation="100"
|
||||
>
|
||||
<template #item="group">
|
||||
<div class="mb-5 ">
|
||||
<!-- Group Title -->
|
||||
<h2 class="group-title">
|
||||
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
|
||||
</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
|
||||
{{ $t("No Monitors") }}
|
||||
</div>
|
||||
|
||||
<!-- Monitor List -->
|
||||
<!-- animation is not working, no idea why -->
|
||||
<Draggable
|
||||
v-model="group.element.monitorList"
|
||||
class="monitor-list"
|
||||
group="same-group"
|
||||
:disabled="!editMode"
|
||||
:animation="100"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="monitor">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<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" />
|
||||
{{ monitor.element.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Draggable from "vuedraggable";
|
||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||
import Uptime from "./Uptime.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
HeartbeatBar,
|
||||
Uptime,
|
||||
},
|
||||
props: {
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showGroupDrag() {
|
||||
return (this.$root.publicGroupList.length >= 2);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
removeGroup(index) {
|
||||
this.$root.publicGroupList.splice(index, 1);
|
||||
},
|
||||
|
||||
removeMonitor(groupIndex, index) {
|
||||
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
.no-monitor-msg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.no-move {
|
||||
transition: transform 0s;
|
||||
}
|
||||
|
||||
.drag {
|
||||
color: #bbb;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
padding: 13px 0 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
73
src/components/Tag.vue
Normal file
73
src/components/Tag.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="tag-wrapper rounded d-inline-flex"
|
||||
:class="{ 'px-3': size == 'normal',
|
||||
'py-1': size == 'normal',
|
||||
'm-2': size == 'normal',
|
||||
'px-2': size == 'sm',
|
||||
'py-0': size == 'sm',
|
||||
'm-1': size == 'sm',
|
||||
}"
|
||||
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||
>
|
||||
<span class="tag-text">{{ displayText }}</span>
|
||||
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||
<font-awesome-icon icon="times" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
remove: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "normal",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayText() {
|
||||
if (this.item.value == "") {
|
||||
return this.item.name;
|
||||
} else {
|
||||
return `${this.item.name}: ${this.item.value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-wrapper {
|
||||
color: white;
|
||||
opacity: 0.85;
|
||||
|
||||
.dark & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
padding-bottom: 1px !important;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
font-size: 0.9em;
|
||||
line-height: 24px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
405
src/components/TagsManager.vue
Normal file
405
src/components/TagsManager.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||
<div class="mb-3 p-1">
|
||||
<tag
|
||||
v-for="item in selectedTags"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:remove="deleteTag"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-add"
|
||||
:disabled="processing"
|
||||
@click.stop="showAddDialog"
|
||||
>
|
||||
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
||||
</button>
|
||||
</div>
|
||||
<div ref="modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<vue-multiselect
|
||||
v-model="newDraftTag.select"
|
||||
class="mb-2"
|
||||
:options="tagOptions"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('Add New below or Select...')"
|
||||
track-by="id"
|
||||
label="name"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>
|
||||
{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #singleLabel="{ option }">
|
||||
<div class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</vue-multiselect>
|
||||
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
||||
<div class="w-50 pe-2">
|
||||
<input v-model="newDraftTag.name" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||
:placeholder="$t('Name')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("Tag with this name already exist.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-50 ps-2">
|
||||
<vue-multiselect
|
||||
v-model="newDraftTag.color"
|
||||
:options="colorOptions"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('color')"
|
||||
track-by="color"
|
||||
label="name"
|
||||
select-label=""
|
||||
deselect-label=""
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #singleLabel="{ option }">
|
||||
<div class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</vue-multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input v-model="newDraftTag.value" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||
:placeholder="$t('value (optional)')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("Tag with this value already exist.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary float-end"
|
||||
:disabled="processing || validateDraftTag.invalid"
|
||||
@click.stop="addDraftTag"
|
||||
>
|
||||
{{ $t("Add") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tag,
|
||||
VueMultiselect,
|
||||
},
|
||||
props: {
|
||||
preSelectedTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
existingTags: [],
|
||||
processing: false,
|
||||
newTags: [],
|
||||
deleteTags: [],
|
||||
newDraftTag: {
|
||||
name: null,
|
||||
select: null,
|
||||
color: null,
|
||||
value: "",
|
||||
invalid: true,
|
||||
nameInvalid: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tagOptions() {
|
||||
const tagOptions = this.existingTags;
|
||||
for (const tag of this.newTags) {
|
||||
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
||||
tagOptions.push(tag);
|
||||
}
|
||||
}
|
||||
return tagOptions;
|
||||
},
|
||||
selectedTags() {
|
||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
||||
},
|
||||
colorOptions() {
|
||||
return [
|
||||
{ name: this.$t("Gray"),
|
||||
color: "#4B5563" },
|
||||
{ name: this.$t("Red"),
|
||||
color: "#DC2626" },
|
||||
{ name: this.$t("Orange"),
|
||||
color: "#D97706" },
|
||||
{ name: this.$t("Green"),
|
||||
color: "#059669" },
|
||||
{ name: this.$t("Blue"),
|
||||
color: "#2563EB" },
|
||||
{ name: this.$t("Indigo"),
|
||||
color: "#4F46E5" },
|
||||
{ name: this.$t("Purple"),
|
||||
color: "#7C3AED" },
|
||||
{ name: this.$t("Pink"),
|
||||
color: "#DB2777" },
|
||||
]
|
||||
},
|
||||
validateDraftTag() {
|
||||
let nameInvalid = false;
|
||||
let valueInvalid = false;
|
||||
let invalid = true;
|
||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
||||
// Undo removing a Tag
|
||||
nameInvalid = false;
|
||||
valueInvalid = false;
|
||||
invalid = false;
|
||||
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||
// Try to create new tag with existing name
|
||||
nameInvalid = true;
|
||||
invalid = true;
|
||||
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
||||
) || (
|
||||
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
||||
)).length > 0) {
|
||||
// Try to add a tag with existing name and value
|
||||
valueInvalid = true;
|
||||
invalid = true;
|
||||
} else if (this.newDraftTag.select != null) {
|
||||
// Select an existing tag, no need to validate
|
||||
invalid = false;
|
||||
valueInvalid = false;
|
||||
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
|
||||
// Missing form inputs
|
||||
nameInvalid = false;
|
||||
invalid = true;
|
||||
} else {
|
||||
// Looks valid
|
||||
invalid = false;
|
||||
nameInvalid = false;
|
||||
valueInvalid = false;
|
||||
}
|
||||
return {
|
||||
invalid,
|
||||
nameInvalid,
|
||||
valueInvalid,
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
this.getExistingTags();
|
||||
},
|
||||
methods: {
|
||||
showAddDialog() {
|
||||
this.modal.show();
|
||||
},
|
||||
getExistingTags() {
|
||||
this.$root.getSocket().emit("getTags", (res) => {
|
||||
if (res.ok) {
|
||||
this.existingTags = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg)
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteTag(item) {
|
||||
if (item.new) {
|
||||
// Undo Adding a new Tag
|
||||
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
|
||||
} else {
|
||||
// Remove an Existing Tag
|
||||
this.deleteTags.push(item);
|
||||
}
|
||||
},
|
||||
textColor(option) {
|
||||
if (option.color) {
|
||||
return "white";
|
||||
} else {
|
||||
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||
}
|
||||
},
|
||||
addDraftTag() {
|
||||
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||
if (this.newDraftTag.select != null) {
|
||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
||||
// Undo removing a tag
|
||||
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
||||
} else {
|
||||
// Add an existing Tag
|
||||
this.newTags.push({
|
||||
id: this.newDraftTag.select.id,
|
||||
color: this.newDraftTag.select.color,
|
||||
name: this.newDraftTag.select.name,
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add new Tag
|
||||
this.newTags.push({
|
||||
color: this.newDraftTag.color.color,
|
||||
name: this.newDraftTag.name.trim(),
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
}
|
||||
this.clearDraftTag();
|
||||
},
|
||||
clearDraftTag() {
|
||||
this.newDraftTag = {
|
||||
name: null,
|
||||
select: null,
|
||||
color: null,
|
||||
value: "",
|
||||
invalid: true,
|
||||
nameInvalid: false,
|
||||
};
|
||||
this.modal.hide();
|
||||
},
|
||||
addTagAsync(newTag) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||
});
|
||||
},
|
||||
addMonitorTagAsync(tagId, monitorId, value) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||
});
|
||||
},
|
||||
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||
});
|
||||
},
|
||||
onEnter() {
|
||||
if (!this.validateDraftTag.invalid) {
|
||||
this.addDraftTag();
|
||||
}
|
||||
},
|
||||
async submit(monitorId) {
|
||||
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||
this.processing = true;
|
||||
|
||||
for (const newTag of this.newTags) {
|
||||
let tagId;
|
||||
if (newTag.id == null) {
|
||||
// Create a New Tag
|
||||
let newTagResult;
|
||||
await this.addTagAsync(newTag).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
newTagResult = false;
|
||||
}
|
||||
newTagResult = res.tag;
|
||||
});
|
||||
if (!newTagResult) {
|
||||
// abort
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
tagId = newTagResult.id;
|
||||
// Assign the new ID to the tags of the same name & color
|
||||
this.newTags.map(tag => {
|
||||
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||
tag.id = newTagResult.id;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
tagId = newTag.id;
|
||||
}
|
||||
|
||||
let newMonitorTagResult;
|
||||
// Assign tag to monitor
|
||||
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
newMonitorTagResult = false;
|
||||
}
|
||||
newMonitorTagResult = true;
|
||||
});
|
||||
if (!newMonitorTagResult) {
|
||||
// abort
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const deleteTag of this.deleteTags) {
|
||||
let deleteMonitorTagResult;
|
||||
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
deleteMonitorTagResult = false;
|
||||
}
|
||||
deleteMonitorTagResult = true;
|
||||
});
|
||||
if (!deleteMonitorTagResult) {
|
||||
// abort
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.getExistingTags();
|
||||
this.newTags = [];
|
||||
this.deleteTags = [];
|
||||
this.processing = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn-add {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
178
src/components/TwoFADialog.vue
Normal file
178
src/components/TwoFADialog.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<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 class="modal-title">
|
||||
{{ $t("Setup 2FA") }}
|
||||
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
|
||||
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
|
||||
</h5>
|
||||
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
|
||||
<vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
|
||||
<button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
|
||||
</div>
|
||||
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||
|
||||
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||
{{ $t("Enable 2FA") }}
|
||||
</button>
|
||||
|
||||
<button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
|
||||
{{ $t("Disable 2FA") }}
|
||||
</button>
|
||||
|
||||
<div v-if="uri && twoFAStatus == false" class="mt-3">
|
||||
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
|
||||
<div class="input-group">
|
||||
<input v-model="token" type="text" maxlength="6" class="form-control">
|
||||
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
|
||||
</div>
|
||||
<p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uri && twoFAStatus == false" class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
|
||||
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
|
||||
{{ $t("confirmEnableTwoFAMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
|
||||
{{ $t("confirmDisableTwoFAMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import Confirm from "./Confirm.vue";
|
||||
import VueQrcode from "vue-qrcode"
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
VueQrcode,
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
uri: null,
|
||||
tokenValid: false,
|
||||
twoFAStatus: null,
|
||||
token: null,
|
||||
showURI: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
this.getStatus();
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal.show()
|
||||
},
|
||||
|
||||
confirmEnableTwoFA() {
|
||||
this.$refs.confirmEnableTwoFA.show()
|
||||
},
|
||||
|
||||
confirmDisableTwoFA() {
|
||||
this.$refs.confirmDisableTwoFA.show()
|
||||
},
|
||||
|
||||
prepare2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("prepare2FA", (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.uri = res.uri;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
save2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("save2FA", (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res)
|
||||
this.getStatus();
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
disable2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("disable2FA", (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res)
|
||||
this.getStatus();
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
verifyToken() {
|
||||
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
||||
if (res.ok) {
|
||||
this.tokenValid = res.valid;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||
if (res.ok) {
|
||||
this.twoFAStatus = res.status;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
75
src/components/notifications/SMTP.vue
Normal file
75
src/components/notifications/SMTP.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="secure" class="form-label">Secure</label>
|
||||
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||
<option :value="false">None / STARTTLS (25, 587)</option>
|
||||
<option :value="true">TLS (465)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls-error">
|
||||
Ignore TLS Error
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-cc" class="form-label">CC</label>
|
||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-bcc" class="form-label">BCC</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "smtp",
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
29
src/components/notifications/Teams.vue
Normal file
29
src/components/notifications/Teams.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
|
||||
<input
|
||||
id="teams-webhookurl"
|
||||
v-model="$parent.notification.webhookUrl"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<div class="form-text">
|
||||
You can learn how to create a webhook url
|
||||
<a
|
||||
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
|
||||
target="_blank"
|
||||
>here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: "teams",
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
96
src/components/notifications/Telegram.vue
Normal file
96
src/components/notifications/Telegram.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<div class="form-text">
|
||||
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required>
|
||||
<button v-if="$parent.notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
|
||||
{{ $t("Auto Get") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<template v-if="$parent.notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "telegram",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
telegramGetUpdatesURL() {
|
||||
let token = "<YOUR BOT TOKEN HERE>"
|
||||
|
||||
if (this.$parent.notification.telegramBotToken) {
|
||||
token = this.$parent.notification.telegramBotToken;
|
||||
}
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
async autoGetTelegramChatID() {
|
||||
try {
|
||||
let res = await axios.get(this.telegramGetUpdatesURL)
|
||||
|
||||
if (res.data.result.length >= 1) {
|
||||
let update = res.data.result[res.data.result.length - 1]
|
||||
|
||||
if (update.channel_post) {
|
||||
this.notification.telegramChatID = update.channel_post.chat.id;
|
||||
} else if (update.message) {
|
||||
this.notification.telegramChatID = update.message.chat.id;
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
52
src/i18n.js
Normal file
52
src/i18n.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import bgBG from "./languages/bg-BG";
|
||||
import daDK from "./languages/da-DK";
|
||||
import deDE from "./languages/de-DE";
|
||||
import en from "./languages/en";
|
||||
import esEs from "./languages/es-ES";
|
||||
import ptBR from "./languages/pt-BR";
|
||||
import etEE from "./languages/et-EE";
|
||||
import frFR from "./languages/fr-FR";
|
||||
import itIT from "./languages/it-IT";
|
||||
import ja from "./languages/ja";
|
||||
import koKR from "./languages/ko-KR";
|
||||
import nlNL from "./languages/nl-NL";
|
||||
import pl from "./languages/pl";
|
||||
import ruRU from "./languages/ru-RU";
|
||||
import sr from "./languages/sr";
|
||||
import srLatn from "./languages/sr-latn";
|
||||
import trTR from "./languages/tr-TR";
|
||||
import svSE from "./languages/sv-SE";
|
||||
import zhCN from "./languages/zh-CN";
|
||||
import zhHK from "./languages/zh-HK";
|
||||
|
||||
const languageList = {
|
||||
en,
|
||||
"zh-HK": zhHK,
|
||||
"bg-BG": bgBG,
|
||||
"de-DE": deDE,
|
||||
"nl-NL": nlNL,
|
||||
"es-ES": esEs,
|
||||
"pt-BR": ptBR,
|
||||
"fr-FR": frFR,
|
||||
"it-IT": itIT,
|
||||
"ja": ja,
|
||||
"da-DK": daDK,
|
||||
"sr": sr,
|
||||
"sr-latn": srLatn,
|
||||
"sv-SE": svSE,
|
||||
"tr-TR": trTR,
|
||||
"ko-KR": koKR,
|
||||
"ru-RU": ruRU,
|
||||
"zh-CN": zhCN,
|
||||
"pl": pl,
|
||||
"et-EE": etEE,
|
||||
};
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: localStorage.locale || "en",
|
||||
fallbackLocale: "en",
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
messages: languageList,
|
||||
});
|
64
src/icon.js
64
src/icon.js
@@ -1,10 +1,60 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp } from "@fortawesome/free-solid-svg-icons"
|
||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
// Add Free Font Awesome Icons here
|
||||
// Add Free Font Awesome Icons
|
||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp);
|
||||
import {
|
||||
faArrowAltCircleUp,
|
||||
faCog,
|
||||
faEdit,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faList,
|
||||
faPause,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTachometerAlt,
|
||||
faTimes,
|
||||
faTimesCircle,
|
||||
faTrash,
|
||||
faCheckCircle,
|
||||
faStream,
|
||||
faSave,
|
||||
faExclamationCircle,
|
||||
faBullhorn,
|
||||
faArrowsAltV,
|
||||
faUnlink,
|
||||
faQuestionCircle,
|
||||
faImages, faUpload,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
faArrowAltCircleUp,
|
||||
faCog,
|
||||
faEdit,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faList,
|
||||
faPause,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTachometerAlt,
|
||||
faTimes,
|
||||
faTimesCircle,
|
||||
faTrash,
|
||||
faCheckCircle,
|
||||
faStream,
|
||||
faSave,
|
||||
faExclamationCircle,
|
||||
faBullhorn,
|
||||
faArrowsAltV,
|
||||
faUnlink,
|
||||
faQuestionCircle,
|
||||
faImages,
|
||||
faUpload,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
||||
export { FontAwesomeIcon }
|
||||
|
@@ -1,18 +1,14 @@
|
||||
# How to translate
|
||||
|
||||
1. Fork this repo.
|
||||
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
|
||||
3. `npm run update-language-files --base-lang=de-DE`
|
||||
6. Your language file should be filled in. You can translate now.
|
||||
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||
8. Import your language file in `src/main.js` and add it to `languageList` constant.
|
||||
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||
|
||||
|
||||
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.
|
||||
4. Your language file should be filled in. You can translate now.
|
||||
5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||
6. Import your language file in `src/i18n.js` and add it to `languageList` constant.
|
||||
7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||
|
||||
One of good examples:
|
||||
https://github.com/louislam/uptime-kuma/pull/316/files
|
||||
|
||||
|
||||
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
||||
|
||||
|
181
src/languages/bg-BG.js
Normal file
181
src/languages/bg-BG.js
Normal file
@@ -0,0 +1,181 @@
|
||||
export default {
|
||||
languageName: "Български",
|
||||
checkEverySecond: "Проверявай на всеки {0} секунди.",
|
||||
retryCheckEverySecond: "Повторен опит на всеки {0} секунди.",
|
||||
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие",
|
||||
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
|
||||
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.",
|
||||
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
|
||||
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
|
||||
passwordNotMatchMsg: "Повторената парола не съвпада.",
|
||||
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.",
|
||||
keywordDescription: "Търсете ключова дума в обикновен html или JSON отговор - чувствителна е към регистъра",
|
||||
pauseDashboardHome: "Пауза",
|
||||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
||||
deleteNotificationMsg: "Наистина ли желаете да изтриете известието за всички монитори?",
|
||||
resoverserverDescription: "Cloudflare е сървърът по подразбиране, можете да промените сървъра по всяко време.",
|
||||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
||||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
||||
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да изключите известието за всеки отделен монитор.",
|
||||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
||||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
||||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
||||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако искате да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
|
||||
confirmImportMsg: "Сигурни ли сте за импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
||||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
||||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
||||
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?",
|
||||
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?",
|
||||
Settings: "Настройки",
|
||||
Dashboard: "Табло",
|
||||
"New Update": "Нова актуализация",
|
||||
Language: "Език",
|
||||
Appearance: "Изглед",
|
||||
Theme: "Тема",
|
||||
General: "Общи",
|
||||
Version: "Версия",
|
||||
"Check Update On GitHub": "Провери за актуализация в GitHub",
|
||||
List: "Списък",
|
||||
Add: "Добави",
|
||||
"Add New Monitor": "Добави монитор",
|
||||
"Quick Stats": "Кратка статистика",
|
||||
Up: "Достъпни",
|
||||
Down: "Недостъпни",
|
||||
Pending: "В изчакване",
|
||||
Unknown: "Неизвестни",
|
||||
Pause: "В пауза",
|
||||
Name: "Име",
|
||||
Status: "Статус",
|
||||
DateTime: "Дата и час",
|
||||
Message: "Съобщение",
|
||||
"No important events": "Няма важни събития",
|
||||
Resume: "Възобнови",
|
||||
Edit: "Редактирай",
|
||||
Delete: "Изтрий",
|
||||
Current: "Текущ",
|
||||
Uptime: "Време на работа",
|
||||
"Cert Exp.": "Вал. сертификат",
|
||||
days: "дни",
|
||||
day: "ден",
|
||||
"-day": "-ден",
|
||||
hour: "час",
|
||||
"-hour": "-час",
|
||||
Response: "Отговор",
|
||||
Ping: "Пинг",
|
||||
"Monitor Type": "Монитор тип",
|
||||
Keyword: "Ключова дума",
|
||||
"Friendly Name": "Псевдоним",
|
||||
URL: "URL Адрес",
|
||||
Hostname: "Име на хост",
|
||||
Port: "Порт",
|
||||
"Heartbeat Interval": "Честота на проверка",
|
||||
Retries: "Повторни опити",
|
||||
"Heartbeat Retry Interval": "Честота на повторните опити",
|
||||
Advanced: "Разширени",
|
||||
"Upside Down Mode": "Обърнат режим",
|
||||
"Max. Redirects": "Макс. брой пренасочвания",
|
||||
"Accepted Status Codes": "Допустими статус кодове",
|
||||
Save: "Запази",
|
||||
Notifications: "Известявания",
|
||||
"Not available, please setup.": "Не е налично. Моля, настройте.",
|
||||
"Setup Notification": "Настройка за известяване",
|
||||
Light: "Светла",
|
||||
Dark: "Тъмна",
|
||||
Auto: "Автоматично",
|
||||
"Theme - Heartbeat Bar": "Тема - поле проверки",
|
||||
Normal: "Нормално",
|
||||
Bottom: "Долу",
|
||||
None: "Без",
|
||||
Timezone: "Часова зона",
|
||||
"Search Engine Visibility": "Видимост за търсачки",
|
||||
"Allow indexing": "Разреши индексиране",
|
||||
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките",
|
||||
"Change Password": "Промени парола",
|
||||
"Current Password": "Текуща парола",
|
||||
"New Password": "Нова парола",
|
||||
"Repeat New Password": "Повторете новата парола",
|
||||
"Update Password": "Актуализирай парола",
|
||||
"Disable Auth": "Изключи удостоверяване",
|
||||
"Enable Auth": "Включи удостоверяване",
|
||||
Logout: "Изход от профила",
|
||||
Leave: "Напускам",
|
||||
"I understand, please disable": "Разбирам. Моля, изключи",
|
||||
Confirm: "Потвърди",
|
||||
Yes: "Да",
|
||||
No: "Не",
|
||||
Username: "Потребител",
|
||||
Password: "Парола",
|
||||
"Remember me": "Запомни ме",
|
||||
Login: "Вход",
|
||||
"No Monitors, please": "Моля, без монитори",
|
||||
"add one": "добави един",
|
||||
"Notification Type": "Тип известяване",
|
||||
Email: "Имейл",
|
||||
Test: "Тест",
|
||||
"Certificate Info": "Информация за сертификат",
|
||||
"Resolver Server": "Преобразуващ (DNS) сървър",
|
||||
"Resource Record Type": "Тип запис",
|
||||
"Last Result": "Последен резултат",
|
||||
"Create your admin account": "Създаване на администриращ акаунт",
|
||||
"Repeat Password": "Повторете паролата",
|
||||
"Import Backup": "Импорт на архив",
|
||||
"Export Backup": "Експорт на архив",
|
||||
Export: "Експорт",
|
||||
Import: "Импорт",
|
||||
respTime: "Време за отговор (ms)",
|
||||
notAvailableShort: "Няма",
|
||||
"Default enabled": "Включен по подразбиране",
|
||||
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
|
||||
Create: "Създай",
|
||||
"Clear Data": "Изчисти данни",
|
||||
Events: "Събития",
|
||||
Heartbeats: "Проверки",
|
||||
"Auto Get": "Автоматияно получаване",
|
||||
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
|
||||
backupDescription2: "PS: Данни за история и събития не са включени.",
|
||||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||
alertNoFile: "Моля, изберете файл за импортиране.",
|
||||
alertWrongFileType: "Моля, изберете JSON файл.",
|
||||
"Clear all statistics": "Изчисти всички статистики",
|
||||
"Skip existing": "Пропусни съществуващите",
|
||||
Overwrite: "Презапиши",
|
||||
Options: "Опции",
|
||||
"Keep both": "Запази двете",
|
||||
"Verify Token": "Проверка на токен код",
|
||||
"Setup 2FA": "Настройка 2FA",
|
||||
"Enable 2FA": "Включи 2FA",
|
||||
"Disable 2FA": "Изключи 2FA",
|
||||
"2FA Settings": "Настройки 2FA",
|
||||
"Two Factor Authentication": "Двуфакторно удостоверяване",
|
||||
Active: "Активно",
|
||||
Inactive: "Неактивно",
|
||||
Token: "Токен код",
|
||||
"Show URI": "Покажи URI",
|
||||
Tags: "Етикети",
|
||||
"Add New below or Select...": "Добави нов по-долу или избери...",
|
||||
"Tag with this name already exist.": "Етикет с това име вече съществува.",
|
||||
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.",
|
||||
color: "цвят",
|
||||
"value (optional)": "стойност (по желание)",
|
||||
Gray: "Сиво",
|
||||
Red: "Червено",
|
||||
Orange: "Оранжево",
|
||||
Green: "Зелено",
|
||||
Blue: "Синьо",
|
||||
Indigo: "Индиго",
|
||||
Purple: "Лилаво",
|
||||
Pink: "Розово",
|
||||
"Search...": "Търси...",
|
||||
"Avg. Ping": "Ср. пинг",
|
||||
"Avg. Response": "Ср. отговор",
|
||||
"Entry Page": "Основна страница",
|
||||
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.",
|
||||
"No Services": "Няма Услуги",
|
||||
"All Systems Operational": "Всички системи функционират",
|
||||
"Partially Degraded Service": "Частично влошена услуга",
|
||||
"Degraded Service": "Влошена услуга",
|
||||
"Add Group": "Добави група",
|
||||
"Add a monitor": "Добави монитор",
|
||||
"Edit Status Page": "Редактирай статус страница",
|
||||
"Go to Dashboard": "Към Таблото",
|
||||
};
|
@@ -17,8 +17,8 @@ export default {
|
||||
Down: "Inaktiv",
|
||||
Pending: "Afventer",
|
||||
Unknown: "Ukendt",
|
||||
Pause: "Pause",
|
||||
pauseDashboardHome: "Pauset",
|
||||
Pause: "Stands",
|
||||
pauseDashboardHome: "Standset",
|
||||
Name: "Navn",
|
||||
Status: "Status",
|
||||
DateTime: "Dato / Tid",
|
||||
@@ -36,8 +36,7 @@ export default {
|
||||
hour: "Timer",
|
||||
"-hour": "-Timer",
|
||||
checkEverySecond: "Tjek hvert {0} sekund",
|
||||
"Avg.": "Gennemsnit",
|
||||
Response: " Respons",
|
||||
Response: "Respons",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Overvåger Type",
|
||||
Keyword: "Nøgleord",
|
||||
@@ -103,10 +102,81 @@ export default {
|
||||
"Resolver Server": "Navne-server",
|
||||
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
|
||||
"Last Result": "Seneste resultat",
|
||||
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
|
||||
pauseMonitorMsg: "Er du sikker på, at du vil standse Overvågeren?",
|
||||
"Create your admin account": "Opret din administratorkonto",
|
||||
"Repeat Password": "Gentag adgangskoden",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
respTime: "Resp. Tid (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Opret",
|
||||
clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?",
|
||||
clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?",
|
||||
confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?",
|
||||
"Clear Data": "Ryd Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto-hent",
|
||||
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
|
||||
"Default enabled": "Standard aktiveret",
|
||||
"Also apply to existing monitors": "Anvend også på eksisterende overvågere",
|
||||
Export: "Eksport",
|
||||
Import: "Import",
|
||||
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
|
||||
backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.",
|
||||
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
|
||||
alertNoFile: "Vælg en fil der skal importeres.",
|
||||
alertWrongFileType: "Vælg venligst en JSON-fil.",
|
||||
twoFAVerifyLabel: "Indtast venligst dit token for at bekræfte, at 2FA fungerer",
|
||||
tokenValidSettingsMsg: "Token er gyldigt! Du kan nu gemme 2FA -indstillingerne.",
|
||||
confirmEnableTwoFAMsg: "Er du sikker på at du vil aktivere 2FA?",
|
||||
confirmDisableTwoFAMsg: "Er du sikker på at du vil deaktivere 2FA?",
|
||||
"Apply on all existing monitors": "Anvend på alle eksisterende overvågere",
|
||||
"Verify Token": "Verificere Token",
|
||||
"Setup 2FA": "Opsæt 2FA",
|
||||
"Enable 2FA": "Aktiver 2FA",
|
||||
"Disable 2FA": "Deaktiver 2FA",
|
||||
"2FA Settings": "2FA Indstillinger",
|
||||
"Two Factor Authentication": "To-Faktor Autentificering",
|
||||
Active: "Aktive",
|
||||
Inactive: "Inaktive",
|
||||
Token: "Token",
|
||||
"Show URI": "Vis URI",
|
||||
"Clear all statistics": "Ryd alle Statistikker",
|
||||
retryCheckEverySecond: "Prøv igen hvert {0} sekund.",
|
||||
importHandleDescription: "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
|
||||
confirmImportMsg: "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Gentagelsesinterval",
|
||||
"Import Backup": "Importer Backup",
|
||||
"Export Backup": "Eksporter Backup",
|
||||
"Skip existing": "Spring over eksisterende",
|
||||
Overwrite: "Overskriv",
|
||||
Options: "Valgmuligheder",
|
||||
"Keep both": "Behold begge",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
|
||||
"Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
|
||||
"Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
|
||||
color: "farve",
|
||||
"value (optional)": "værdi (valgfri)",
|
||||
Gray: "Grå",
|
||||
Red: "Rød",
|
||||
Orange: "Orange",
|
||||
Green: "Grøn",
|
||||
Blue: "Blå",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Lilla",
|
||||
Pink: "Pink",
|
||||
"Search...": "Søg...",
|
||||
"Avg. Ping": "Gns. Ping",
|
||||
"Avg. Response": "Gns. Respons",
|
||||
"Entry Page": "Entry Side",
|
||||
"statusPageNothing": "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
|
||||
"No Services": "Ingen Tjenester",
|
||||
"All Systems Operational": "Alle Systemer i Drift",
|
||||
"Partially Degraded Service": "Delvist Forringet Service",
|
||||
"Degraded Service": "Forringet Service",
|
||||
"Add Group": "Tilføj Gruppe",
|
||||
"Add a monitor": "Tilføj en Overvåger",
|
||||
"Edit Status Page": "Rediger Statusside",
|
||||
"Go to Dashboard": "Gå til Dashboard",
|
||||
};
|
||||
|
@@ -36,8 +36,7 @@ export default {
|
||||
hour: "Stunde",
|
||||
"-hour": "-Stunden",
|
||||
checkEverySecond: "Überprüfe alle {0} Sekunden",
|
||||
"Avg.": "Durchschn. ",
|
||||
Response: " Antwortzeit",
|
||||
Response: "Antwortzeit",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Monitor Typ",
|
||||
Keyword: "Schlüsselwort",
|
||||
@@ -104,9 +103,79 @@ export default {
|
||||
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
|
||||
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?",
|
||||
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?",
|
||||
"Clear Data": "Lösche Daten",
|
||||
Events: "Ereignisse",
|
||||
Heartbeats: "Statistiken",
|
||||
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin Konto",
|
||||
"Repeat Password": "Wiederhole das Passwort",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
respTime: "Antw. Zeit (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "Standardmäßig aktiviert",
|
||||
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
|
||||
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
|
||||
Create: "Erstellen",
|
||||
"Auto Get": "Auto Get",
|
||||
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
|
||||
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
|
||||
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
||||
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
||||
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
||||
"Clear all statistics": "Lösche alle Statistiken",
|
||||
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
|
||||
"Skip existing": "Vorhandene überspringen",
|
||||
Overwrite: "Überschreiben",
|
||||
Options: "Optionen",
|
||||
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
|
||||
"Keep both": "Beide behalten",
|
||||
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
|
||||
"Verify Token": "Token verifizieren",
|
||||
"Setup 2FA": "2FA Einrichten",
|
||||
"Enable 2FA": "2FA Aktivieren",
|
||||
"Disable 2FA": "2FA deaktivieren",
|
||||
"2FA Settings": "2FA Einstellungen",
|
||||
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
|
||||
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
|
||||
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
|
||||
"Two Factor Authentication": "Zwei Faktor Authentifizierung",
|
||||
Active: "Aktiv",
|
||||
Inactive: "Inaktiv",
|
||||
Token: "Token",
|
||||
"Show URI": "URI Anzeigen",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...",
|
||||
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.",
|
||||
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.",
|
||||
color: "Farbe",
|
||||
"value (optional)": "Wert (Optional)",
|
||||
Gray: "Grau",
|
||||
Red: "Rot",
|
||||
Orange: "Orange",
|
||||
Green: "Grün",
|
||||
Blue: "Blau",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Lila",
|
||||
Pink: "Pink",
|
||||
"Search...": "Suchen...",
|
||||
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall",
|
||||
retryCheckEverySecond: "Versuche alle {0} Sekunden",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Avg. Ping": "Durchsch. Ping",
|
||||
"Avg. Response": "Durchsch. Antwort",
|
||||
"Entry Page": "Einstiegsseite",
|
||||
statusPageNothing: "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
|
||||
"No Services": "Keine Dienste",
|
||||
"All Systems Operational": "Alle Systeme Betriebsbereit",
|
||||
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
|
||||
"Degraded Service": "Eingeschränkter Dienst",
|
||||
"Add Group": "Gruppe hinzufügen",
|
||||
"Add a monitor": "Monitor hinzufügen",
|
||||
"Edit Status Page": "Bearbeite Statusseite",
|
||||
"Go to Dashboard": "Gehe zum Dashboard",
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
languageName: "English",
|
||||
checkEverySecond: "Check every {0} seconds.",
|
||||
"Avg.": "Avg. ",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
|
||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
@@ -16,6 +16,16 @@ export default {
|
||||
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.",
|
||||
rrtypeDescription: "Select the RR-Type you want to monitor",
|
||||
pauseMonitorMsg: "Are you sure want to pause?",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
Settings: "Settings",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "New Update",
|
||||
@@ -60,6 +70,7 @@ export default {
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Heartbeat Interval",
|
||||
Retries: "Retries",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
Advanced: "Advanced",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Max. Redirects": "Max. Redirects",
|
||||
@@ -107,6 +118,64 @@ export default {
|
||||
"Last Result": "Last Result",
|
||||
"Create your admin account": "Create your admin account",
|
||||
"Repeat Password": "Repeat Password",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "Default enabled",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
Create: "Create",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "Español",
|
||||
checkEverySecond: "Comprobar cada {0} segundos.",
|
||||
"Avg.": "Media. ",
|
||||
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
|
||||
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
|
||||
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
|
||||
@@ -32,7 +31,7 @@ export default {
|
||||
Up: "Funcional",
|
||||
Down: "Caído",
|
||||
Pending: "Pendiente",
|
||||
Unknown: "Desconociso",
|
||||
Unknown: "Desconocido",
|
||||
Pause: "Pausa",
|
||||
Name: "Nombre",
|
||||
Status: "Estado",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "Crea tu cuenta de administrador",
|
||||
"Repeat Password": "Repetir contraseña",
|
||||
respTime: "Tiempo de resp. (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
182
src/languages/et-EE.js
Normal file
182
src/languages/et-EE.js
Normal file
@@ -0,0 +1,182 @@
|
||||
export default {
|
||||
languageName: "eesti",
|
||||
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
|
||||
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
|
||||
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
|
||||
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
|
||||
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
|
||||
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
|
||||
passwordNotMatchMsg: "Salasõnad ei kattu.",
|
||||
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
|
||||
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
|
||||
pauseDashboardHome: "Seismas",
|
||||
deleteMonitorMsg: "Kas soovid eemaldada seire?",
|
||||
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
|
||||
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
|
||||
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
|
||||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||
Settings: "Seaded",
|
||||
Dashboard: "Töölaud",
|
||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||
Language: "Keel",
|
||||
Appearance: "Välimus",
|
||||
Theme: "Teema",
|
||||
General: "Üldine",
|
||||
Version: "Versioon",
|
||||
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
|
||||
List: "Nimekiri",
|
||||
Add: "Lisa",
|
||||
"Add New Monitor": "Seire lisamine",
|
||||
"Quick Stats": "Ülevaade",
|
||||
Up: "Töökorras",
|
||||
Down: "Rikkis",
|
||||
Pending: "Määramisel",
|
||||
Unknown: "Teadmata",
|
||||
Pause: "Seiskamine",
|
||||
Name: "Nimi",
|
||||
Status: "Olek",
|
||||
DateTime: "Kuupäev",
|
||||
Message: "Tulemus",
|
||||
"No important events": "Märkimisväärsed juhtumid puuduvad.",
|
||||
Resume: "Taasta",
|
||||
Edit: "Muutmine",
|
||||
Delete: "Eemalda",
|
||||
Current: "Hetkeseisund",
|
||||
Uptime: "Eluiga",
|
||||
"Cert Exp.": "Sert. aegumine",
|
||||
days: "päeva",
|
||||
day: "päev",
|
||||
"-day": "-päev",
|
||||
hour: "tund",
|
||||
"-hour": "-tund",
|
||||
Response: "Vastus",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Seire tüüp",
|
||||
Keyword: "Võtmesõna",
|
||||
"Friendly Name": "Sõbralik nimi",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Tukse sagedus",
|
||||
Retries: "Korduskatsed",
|
||||
Advanced: "Rohkem",
|
||||
"Upside Down Mode": "Tagurpidi seire",
|
||||
"Max. Redirects": "Max. ümbersuunamine",
|
||||
"Accepted Status Codes": "Kõlblikud HTTP koodid",
|
||||
Save: "Salvesta",
|
||||
Notifications: "Teavitused",
|
||||
"Not available, please setup.": "Ühtegi teavitusteenust pole saadaval.",
|
||||
"Setup Notification": "Lisa teavitusteenus",
|
||||
Light: "hele",
|
||||
Dark: "tume",
|
||||
Auto: "automaatne",
|
||||
"Theme - Heartbeat Bar": "Teemasäte — tuksete riba",
|
||||
Normal: "tavaline",
|
||||
Bottom: "all",
|
||||
None: "puudub",
|
||||
Timezone: "Ajatsoon",
|
||||
"Search Engine Visibility": "Otsimootorite ligipääs",
|
||||
"Allow indexing": "Luba indekseerimine",
|
||||
"Discourage search engines from indexing site": "Keela selle saidi indekseerimine otsimootorite poolt",
|
||||
"Change Password": "Muuda parooli",
|
||||
"Current Password": "praegune parool",
|
||||
"New Password": "uus parool",
|
||||
"Repeat New Password": "korda salasõna",
|
||||
"Update Password": "Uuenda salasõna",
|
||||
"Disable Auth": "Lülita autentimine välja",
|
||||
"Enable Auth": "Lülita autentimine sisse",
|
||||
Logout: "Logi välja",
|
||||
Leave: "Lahku",
|
||||
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",
|
||||
Confirm: "Kinnita",
|
||||
Yes: "Jah",
|
||||
No: "Ei",
|
||||
Username: "kasutajanimi",
|
||||
Password: "parool",
|
||||
"Remember me": "Mäleta mind",
|
||||
Login: "Logi sisse",
|
||||
"No Monitors, please": "Seired puuduvad.",
|
||||
"add one": "Lisa esimene",
|
||||
"Notification Type": "Teavituse tüüp",
|
||||
Email: "e-posti aadress",
|
||||
Test: "Saada prooviteavitus",
|
||||
"Certificate Info": "Sertifikaadi teave",
|
||||
"Resolver Server": "Server, mis vastab DNS päringutele.",
|
||||
"Resource Record Type": "DNS kirje tüüp",
|
||||
"Last Result": "Viimane",
|
||||
"Create your admin account": "Admininstraatori konto loomine",
|
||||
"Repeat Password": "korda salasõna",
|
||||
respTime: "Reageerimisaeg (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
|
||||
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
|
||||
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
|
||||
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
|
||||
Export: "Eksport",
|
||||
Import: "Import",
|
||||
"Default enabled": "Kasuta vaikimisi",
|
||||
"Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
|
||||
Create: "Loo konto",
|
||||
"Clear Data": "Eemalda andmed",
|
||||
Events: "Sündmused",
|
||||
Heartbeats: "Tuksed",
|
||||
"Auto Get": "Hangi automaatselt",
|
||||
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
|
||||
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
|
||||
backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
|
||||
alertNoFile: "Palun lisa fail, mida importida.",
|
||||
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
@@ -36,7 +36,6 @@ export default {
|
||||
hour: "Heure",
|
||||
"-hour": "Heures",
|
||||
checkEverySecond: "Vérifier toutes les {0} secondes",
|
||||
"Avg.": "Moyen",
|
||||
Response: "Temps de réponse",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Type de Sonde",
|
||||
@@ -66,7 +65,7 @@ export default {
|
||||
"Theme - Heartbeat Bar": "Voir les services surveillés",
|
||||
Normal: "Général",
|
||||
Bottom: "En dessous",
|
||||
None: "Non",
|
||||
None: "Rien",
|
||||
Timezone: "Fuseau Horaire",
|
||||
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
|
||||
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "Créez votre compte administrateur",
|
||||
"Repeat Password": "Répéter le mot de passe",
|
||||
respTime: "Temps de réponse (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Créer",
|
||||
clearEventsMsg: "Êtes-vous sûr de vouloir supprimer tous les événements pour cette sonde ?",
|
||||
clearHeartbeatsMsg: "Êtes-vous sûr de vouloir supprimer tous les vérifications pour cette sonde ? Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "tes-vous sûr de vouloir supprimer tous les statistiques ?",
|
||||
"Clear Data": "Effacer les données",
|
||||
Events: "Evénements",
|
||||
Heartbeats: "Vérfications",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.",
|
||||
"Default enabled": "Activé par défaut",
|
||||
"Also apply to existing monitors": "S'applique également aux sondes existantes",
|
||||
Export: "Exporter",
|
||||
Import: "Importer",
|
||||
backupDescription: "Vous pouvez sauvegarder toutes les sondes et toutes les notifications dans un fichier JSON.",
|
||||
backupDescription2: "PS: Les données relatives à l'historique et aux événements ne sont pas incluses.",
|
||||
backupDescription3: "Les données sensibles telles que les jetons de notification sont incluses dans le fichier d'exportation, veuillez les conserver soigneusement.",
|
||||
alertNoFile: "Veuillez sélectionner un fichier à importer.",
|
||||
alertWrongFileType: "Veuillez sélectionner un fichier JSON à importer.",
|
||||
twoFAVerifyLabel: "Veuillez saisir votre jeton pour vérifier que le système 2FA fonctionne.",
|
||||
tokenValidSettingsMsg: "Le jeton est valide ! Vous pouvez maintenant sauvegarder les paramètres 2FA.",
|
||||
confirmEnableTwoFAMsg: "Êtes-vous sûr de vouloir activer le 2FA ?",
|
||||
confirmDisableTwoFAMsg: "Êtes-vous sûr de vouloir désactiver le 2FA ?",
|
||||
"Apply on all existing monitors": "Appliquer sur toutes les sondes existantes",
|
||||
"Verify Token": "Vérifier le jeton",
|
||||
"Setup 2FA": "Configurer 2FA",
|
||||
"Enable 2FA": "Activer 2FA",
|
||||
"Disable 2FA": "Désactiver 2FA",
|
||||
"2FA Settings": "Paramètres 2FA",
|
||||
"Two Factor Authentication": "Authentification à deux facteurs",
|
||||
Active: "Actif",
|
||||
Inactive: "Inactif",
|
||||
Token: "Jeton",
|
||||
"Show URI": "Afficher l'URI",
|
||||
"Clear all statistics": "Effacer touutes les statistiques",
|
||||
retryCheckEverySecond: "Réessayer toutes les {0} secondes.",
|
||||
importHandleDescription: "Choisissez 'Ignorer l'existant' si vous voulez ignorer chaque sonde ou notification portant le même nom. L'option 'Écraser' supprime tous les sondes et notifications existantes.",
|
||||
confirmImportMsg: "Êtes-vous sûr d'importer la sauvegarde ? Veuillez vous assurer que vous avez sélectionné la bonne option d'importation.",
|
||||
"Heartbeat Retry Interval": "Réessayer l'intervale de vérification",
|
||||
"Import Backup": "Importation de la sauvegarde",
|
||||
"Export Backup": "Exportation de la sauvegarde",
|
||||
"Skip existing": "Sauter l'existant",
|
||||
Overwrite: "Ecraser",
|
||||
Options: "Options",
|
||||
"Keep both": "Garder les deux",
|
||||
Tags: "Étiquettes",
|
||||
"Add New below or Select...": "Ajouter nouveau ci-dessous ou sélectionner...",
|
||||
"Tag with this name already exist.": "Une étiquette portant ce nom existe déjà.",
|
||||
"Tag with this value already exist.": "Une étiquette avec cette valeur existe déjà.",
|
||||
color: "couleur",
|
||||
"value (optional)": "valeur (facultatif)",
|
||||
Gray: "Gris",
|
||||
Red: "Rouge",
|
||||
Orange: "Orange",
|
||||
Green: "Vert",
|
||||
Blue: "Bleu",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Violet",
|
||||
Pink: "Rose",
|
||||
"Search...": "Rechercher...",
|
||||
"Avg. Ping": "Ping moyen",
|
||||
"Avg. Response": "Réponse moyenne",
|
||||
"Entry Page": "Page d'accueil",
|
||||
"statusPageNothing": "Rien ici, veuillez ajouter un groupe ou une sonde.",
|
||||
"No Services": "Aucun service",
|
||||
"All Systems Operational": "Tous les systèmes sont opérationnels",
|
||||
"Partially Degraded Service": "Service partiellement dégradé",
|
||||
"Degraded Service": "Service dégradé",
|
||||
"Add Group": "Ajouter un groupe",
|
||||
"Add a monitor": "Ajouter une sonde",
|
||||
"Edit Status Page": "Modifier la page de statut",
|
||||
"Go to Dashboard": "Accéder au tableau de bord",
|
||||
};
|
181
src/languages/it-IT.js
Normal file
181
src/languages/it-IT.js
Normal file
@@ -0,0 +1,181 @@
|
||||
export default {
|
||||
languageName: "Italiano (Italian)",
|
||||
checkEverySecond: "controlla ogni {0} secondi",
|
||||
retryCheckEverySecond: "Riprova ogni {0} secondi.",
|
||||
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
|
||||
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
|
||||
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
|
||||
maxRedirectDescription: "Numero massimo di redirezionamenti consentito. Per disabilitare impostare \"0\".",
|
||||
acceptedStatusCodesDescription: "Inserire i codici di stato considerati come risposte corrette.",
|
||||
passwordNotMatchMsg: "La password non coincide.",
|
||||
notificationDescription: "Assegnare la notifica a uno o più elementi monitorati per metterla in funzione.",
|
||||
keywordDescription: "Cerca la parola chiave nella risposta in html o JSON e fai distinzione tra maiuscole e minuscole",
|
||||
pauseDashboardHome: "In Pausa",
|
||||
deleteMonitorMsg: "Si è certi di voler eliminare questo monitoraggio?",
|
||||
deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?",
|
||||
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
|
||||
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
|
||||
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
|
||||
enableDefaultNotificationDescription: "Per ogni nuovo monitoraggio questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica separatamente per ogni monitoraggio.",
|
||||
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
|
||||
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
|
||||
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
|
||||
importHandleDescription: "Selezionare 'Ignora gli esistenti' si vuole ignorare l'importazione dei monitoraggi o delle notifiche con lo stesso nome. 'Sovrascrivi' eliminerà ogni monitoraggio e notifica esistente.",
|
||||
confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.",
|
||||
twoFAVerifyLabel: "Scrivi il token per verificare che l'autenticazione a due fattori funzioni",
|
||||
tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.",
|
||||
confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?",
|
||||
confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?",
|
||||
Settings: "Impostazioni",
|
||||
Dashboard: "Cruscotto",
|
||||
"New Update": "Nuovo Aggiornamento Disponibile",
|
||||
Language: "Lingua",
|
||||
Appearance: "Aspetto",
|
||||
Theme: "Tema",
|
||||
General: "Generali",
|
||||
Version: "Versione",
|
||||
"Check Update On GitHub": "Controlla aggiornamenti su GitHub",
|
||||
List: "Lista",
|
||||
Add: "Aggiungi",
|
||||
"Add New Monitor": "Aggiungi un nuovo monitoraggio",
|
||||
"Quick Stats": "Statistiche rapide",
|
||||
Up: "Su",
|
||||
Down: "Giù",
|
||||
Pending: "Pendente",
|
||||
Unknown: "Sconosciuti",
|
||||
Pause: "Metti in Pausa",
|
||||
Name: "Nome",
|
||||
Status: "Stato",
|
||||
DateTime: "Data e Ora",
|
||||
Message: "Messaggio",
|
||||
"No important events": "Nessun evento importante",
|
||||
Resume: "Riprendi",
|
||||
Edit: "Modifica",
|
||||
Delete: "Elimina",
|
||||
Current: "Corrente",
|
||||
Uptime: "Tempo di attività",
|
||||
"Cert Exp.": "Scadenza certificato",
|
||||
days: "giorni",
|
||||
day: "giorno",
|
||||
"-day": "-giorni",
|
||||
hour: "ora",
|
||||
"-hour": "-ore",
|
||||
Response: "Risposta",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Tipo di Monitoraggio",
|
||||
Keyword: "Parola chiave",
|
||||
"Friendly Name": "Nome Amichevole",
|
||||
URL: "URL",
|
||||
Hostname: "Nome Host",
|
||||
Port: "Porta",
|
||||
"Heartbeat Interval": "Intervallo di controllo",
|
||||
Retries: "Tentativi",
|
||||
"Heartbeat Retry Interval": "Intervallo tra un tentativo di controllo e l'altro",
|
||||
Advanced: "Avanzate",
|
||||
"Upside Down Mode": "Modalità capovolta",
|
||||
"Max. Redirects": "Reindirizzamenti massimi",
|
||||
"Accepted Status Codes": "Codici di stato accettati",
|
||||
Save: "Salva",
|
||||
Notifications: "Notifiche",
|
||||
"Not available, please setup.": "Non disponibili, da impostare.",
|
||||
"Setup Notification": "Imposta le notifiche",
|
||||
Light: "Chiaro",
|
||||
Dark: "Scuro",
|
||||
Auto: "Automatico",
|
||||
"Theme - Heartbeat Bar": "Tema - Barra di Stato",
|
||||
Normal: "Normale",
|
||||
Bottom: "Sotto",
|
||||
None: "Nessuna",
|
||||
Timezone: "Fuso Orario",
|
||||
"Search Engine Visibility": "Visibilità ai motori di ricerca",
|
||||
"Allow indexing": "Permetti l'indicizzazione",
|
||||
"Discourage search engines from indexing site": "Scoraggia l'indicizzazione da parte dei motori di ricerca",
|
||||
"Change Password": "Cambio Password",
|
||||
"Current Password": "Password Corrente",
|
||||
"New Password": "Nuova Password",
|
||||
"Repeat New Password": "Ripetere la nuova Password",
|
||||
"Update Password": "Modifica Password",
|
||||
"Disable Auth": "Disabilita l'autenticazione",
|
||||
"Enable Auth": "Abilita Autenticazione",
|
||||
Logout: "Esci",
|
||||
Leave: "Annulla",
|
||||
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",
|
||||
Confirm: "Conferma",
|
||||
Yes: "Sì",
|
||||
No: "No",
|
||||
Username: "Nome Utente",
|
||||
Password: "Password",
|
||||
"Remember me": "Ricordami",
|
||||
Login: "Accesso",
|
||||
"No Monitors, please": "Nessun monitoraggio, cortesemente",
|
||||
"add one": "aggiungerne uno",
|
||||
"Notification Type": "Tipo di notifica",
|
||||
Email: "E-mail",
|
||||
Test: "Prova",
|
||||
"Certificate Info": "Informazioni sul certificato",
|
||||
"Resolver Server": "Server DNS",
|
||||
"Resource Record Type": "Tipo di Resource Record",
|
||||
"Last Result": "Ultimo risultato",
|
||||
"Create your admin account": "Crea l'account amministratore",
|
||||
"Repeat Password": "Ripeti Password",
|
||||
"Import Backup": "Importa Backup",
|
||||
"Export Backup": "Esporta Backup",
|
||||
Export: "Esporta",
|
||||
Import: "Importa",
|
||||
respTime: "Tempo di Risposta (ms)",
|
||||
notAvailableShort: "N/D",
|
||||
"Default enabled": "Abilitato di default",
|
||||
"Apply on all existing monitors": "Applica su tutti i monitoraggi",
|
||||
Create: "Crea",
|
||||
"Clear Data": "Cancella dati",
|
||||
Events: "Eventi",
|
||||
Heartbeats: "Controlli",
|
||||
"Auto Get": "Auto Get",
|
||||
backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.",
|
||||
backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi.",
|
||||
backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.",
|
||||
alertNoFile: "Selezionare il file da importare.",
|
||||
alertWrongFileType: "Selezionare un file JSON.",
|
||||
"Clear all statistics": "Pulisci tutte le statistiche",
|
||||
"Skip existing": "Ignora gli esistenti",
|
||||
Overwrite: "Sovrascrivi",
|
||||
Options: "Opzioni",
|
||||
"Keep both": "Mantieni entrambi",
|
||||
"Verify Token": "Verifica Token",
|
||||
"Setup 2FA": "Imposta l'autenticazione a due fattori",
|
||||
"Enable 2FA": "Abilita l'autenticazione a due fattori",
|
||||
"Disable 2FA": "Disabilita l'autenticazione a due fattori",
|
||||
"2FA Settings": "Impostazioni autenticazione a due fattori",
|
||||
"Two Factor Authentication": "Autenticazione a due fattori",
|
||||
Active: "Attivata",
|
||||
Inactive: "Disattivata",
|
||||
Token: "Token",
|
||||
"Show URI": "Mostra URI",
|
||||
Tags: "Etichette",
|
||||
"Add New below or Select...": "Aggiungine una oppure scegli...",
|
||||
"Tag with this name already exist.": "Un'etichetta con questo nome già esiste.",
|
||||
"Tag with this value already exist.": "Un'etichetta con questo valore già esiste.",
|
||||
color: "colori",
|
||||
"value (optional)": "valore (opzionale)",
|
||||
Gray: "Grigio",
|
||||
Red: "Rosso",
|
||||
Orange: "Arancione",
|
||||
Green: "Verde",
|
||||
Blue: "Blu",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Viola",
|
||||
Pink: "Rosa",
|
||||
"Search...": "Cerca...",
|
||||
"Avg. Ping": "Ping medio",
|
||||
"Avg. Response": "Risposta media",
|
||||
"Entry Page": "Entry Page",
|
||||
"statusPageNothing": "Non c'è nulla qui, aggiungere un gruppo oppure un monitoraggio.",
|
||||
"No Services": "Nessun Servizio",
|
||||
"All Systems Operational": "Tutti i sistemi sono operativi",
|
||||
"Partially Degraded Service": "Servizio parzialmente degradato",
|
||||
"Degraded Service": "Servizio degradato",
|
||||
"Add Group": "Aggiungi Gruppo",
|
||||
"Add a monitor": "Aggiungi un monitoraggio",
|
||||
"Edit Status Page": "Modifica pagina di stato",
|
||||
"Go to Dashboard": "Vai al Cruscotto",
|
||||
};
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "日本語",
|
||||
checkEverySecond: "{0}秒ごとにチェックします。",
|
||||
"Avg.": "平均 ",
|
||||
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
|
||||
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
|
||||
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "Create your admin account",
|
||||
"Repeat Password": "Repeat Password",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "한국어",
|
||||
checkEverySecond: "{0} 초마다 체크해요.",
|
||||
"Avg.": "평균 ",
|
||||
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
|
||||
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
|
||||
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "관리자 계정 만들기",
|
||||
"Repeat Password": "비밀번호 재입력",
|
||||
respTime: "응답 시간 (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "Nederlands",
|
||||
checkEverySecond: "Controleer elke {0} seconden.",
|
||||
"Avg.": "Gem. ",
|
||||
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
|
||||
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
|
||||
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
|
||||
@@ -16,6 +15,14 @@ export default {
|
||||
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
|
||||
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
|
||||
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
|
||||
enableDefaultNotificationDescription: "Voor elke nieuwe monitor wordt deze melding standaard ingeschakeld. U kunt de melding nog steeds afzonderlijk uitschakelen voor elke monitor.",
|
||||
clearEventsMsg: "Weet je zeker dat je alle evenementen voor deze monitor wilt verwijderen?",
|
||||
clearHeartbeatsMsg: "Weet je zeker dat je alle heartbeats voor deze monitor wilt verwijderen?",
|
||||
confirmClearStatisticsMsg: "Weet u zeker dat u alle statistieken wilt verwijderen?",
|
||||
twoFAVerifyLabel: "Voer uw 2FA controle token in voor verificatie",
|
||||
tokenValidSettingsMsg: "Token is geldig! U kunt nu de 2FA-instellingen opslaan.",
|
||||
confirmEnableTwoFAMsg: "Weet je zeker dat je 2FA wilt inschakelen?",
|
||||
confirmDisableTwoFAMsg: "Weet je zeker dat je 2FA wilt uitschakelen?",
|
||||
Settings: "Instellingen",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Nieuwe update",
|
||||
@@ -80,7 +87,7 @@ export default {
|
||||
"Allow indexing": "Indexering toestaan",
|
||||
"Discourage search engines from indexing site": "Ontmoedig zoekmachines om de site te indexeren",
|
||||
"Change Password": "Verander wachtwoord",
|
||||
"Current Password": "Huidig wachtwoord",
|
||||
"Current Password": "Huidig wachtwoord",
|
||||
"New Password": "Nieuw wachtwoord",
|
||||
"Repeat New Password": "Herhaal nieuw wachtwoord",
|
||||
"Update Password": "Vernieuw wachtwoord",
|
||||
@@ -107,6 +114,69 @@ export default {
|
||||
"Last Result": "Laatste resultaat",
|
||||
"Create your admin account": "Maak uw beheerdersaccount aan",
|
||||
"Repeat Password": "Herhaal wachtwoord",
|
||||
Export: "Exporteren",
|
||||
Import: "Importeren",
|
||||
respTime: "resp. tijd (ms)",
|
||||
notAvailableShort: "N.v.t."
|
||||
}
|
||||
notAvailableShort: "N.v.t.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Apply on all existing monitors": "Pas toe op alle bestaande monitors",
|
||||
Create: "Aanmaken",
|
||||
"Clear Data": "Data wissen",
|
||||
Events: "Gebeurtenissen",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
backupDescription: "U kunt een back-up maken van alle monitoren en alle meldingen in een JSON-bestand.",
|
||||
backupDescription2: "PS: Geschiedenis- en gebeurtenisgegevens zijn niet inbegrepen.",
|
||||
backupDescription3: "Gevoelige gegevens zoals melding tokens zijn opgenomen in het exportbestand, houd het veilig opgeslagen.",
|
||||
alertNoFile: "Selecteer een bestand om te importeren.",
|
||||
alertWrongFileType: "Selecteer een JSON-bestand.",
|
||||
"Verify Token": "Controleer token",
|
||||
"Setup 2FA": "2FA instellingen",
|
||||
"Enable 2FA": "Schakel 2FA in",
|
||||
"Disable 2FA": "Schakel 2FA uit",
|
||||
"2FA Settings": "2FA-instellingen",
|
||||
"Two Factor Authentication": "Two Factor Authenticatie",
|
||||
Active: "Actief",
|
||||
Inactive: "Inactief",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Token: "Token",
|
||||
"Show URI": "Toon URI",
|
||||
"Clear all statistics": "Wis alle statistieken",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
182
src/languages/pl.js
Normal file
182
src/languages/pl.js
Normal file
@@ -0,0 +1,182 @@
|
||||
export default {
|
||||
languageName: "Polski",
|
||||
checkEverySecond: "Sprawdzam co {0} sekund.",
|
||||
retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
|
||||
ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
|
||||
upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",
|
||||
maxRedirectDescription: "Maksymalna liczba przekierowań do wykonania. Ustaw na 0, aby wyłączyć przekierowania.",
|
||||
acceptedStatusCodesDescription: "Wybierz kody stanu, które są uważane za udaną odpowiedź.",
|
||||
passwordNotMatchMsg: "Powtórzone hasło nie pasuje.",
|
||||
notificationDescription: "Proszę przypisać powiadomienie do monitora(ów), aby zadziałało.",
|
||||
keywordDescription: "Wyszukiwanie słów kluczowych w zwykłym html lub odpowiedzi JSON. Wielkość liter ma znaczenie.",
|
||||
pauseDashboardHome: "Pauza",
|
||||
deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?",
|
||||
deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?",
|
||||
resoverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
|
||||
rrtypeDescription: "Wybierz RR-Type który chcesz monitorować",
|
||||
pauseMonitorMsg: "Czy na pewno chcesz wstrzymać?",
|
||||
Settings: "Ustawienia",
|
||||
Dashboard: "Panel",
|
||||
"New Update": "Nowa aktualizacja",
|
||||
Language: "Język",
|
||||
Appearance: "Wygląd",
|
||||
Theme: "Motyw",
|
||||
General: "Ogólne",
|
||||
Version: "Wersja",
|
||||
"Check Update On GitHub": "Sprawdź aktualizację na GitHub.",
|
||||
List: "Lista",
|
||||
Add: "Dodaj",
|
||||
"Add New Monitor": "Dodaj nowy monitor",
|
||||
"Quick Stats": "Szybkie statystyki",
|
||||
Up: "Online",
|
||||
Down: "Offline",
|
||||
Pending: "Oczekujący",
|
||||
Unknown: "Nieznane",
|
||||
Pause: "Pauza",
|
||||
Name: "Nazwa",
|
||||
Status: "Status",
|
||||
DateTime: "Data i godzina",
|
||||
Message: "Wiadomość",
|
||||
"No important events": "Brak ważnych wydarzeń",
|
||||
Resume: "Wznów",
|
||||
Edit: "Edytuj",
|
||||
Delete: "Usuń",
|
||||
Current: "aktualny",
|
||||
Uptime: "Czas pracy",
|
||||
"Cert Exp.": "Wygaśnięcie certyfikatu",
|
||||
days: "dni",
|
||||
day: "dzień",
|
||||
"-day": " dni",
|
||||
hour: "godzina",
|
||||
"-hour": " godziny",
|
||||
Response: "Odpowiedź",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Typ monitora",
|
||||
Keyword: "Słowo kluczowe",
|
||||
"Friendly Name": "Przyjazna nazwa",
|
||||
URL: "URL",
|
||||
Hostname: "Nazwa hosta",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Interwał bicia serca",
|
||||
Retries: "Prób",
|
||||
Advanced: "Zaawansowane",
|
||||
"Upside Down Mode": "Tryb do góry nogami",
|
||||
"Max. Redirects": "Maks. przekierowania",
|
||||
"Accepted Status Codes": "Akceptowane kody statusu",
|
||||
Save: "Zapisz",
|
||||
Notifications: "Powiadomienia",
|
||||
"Not available, please setup.": "Niedostępne, proszę skonfigurować.",
|
||||
"Setup Notification": "Konfiguracja powiadomień",
|
||||
Light: "Jasny",
|
||||
Dark: "Ciemny",
|
||||
Auto: "Automatyczny",
|
||||
"Theme - Heartbeat Bar": "Motyw - pasek bicia serca",
|
||||
Normal: "Normalne",
|
||||
Bottom: "Na dole",
|
||||
None: "Brak",
|
||||
Timezone: "Strefa czasowa",
|
||||
"Search Engine Visibility": "Widoczność w wyszukiwarce",
|
||||
"Allow indexing": "Pozwól na indeksowanie",
|
||||
"Discourage search engines from indexing site": "Zniechęcaj wyszukiwarki do indeksowania strony",
|
||||
"Change Password": "Zmień hasło",
|
||||
"Current Password": "Aktualne hasło",
|
||||
"New Password": "Nowe hasło",
|
||||
"Repeat New Password": "Powtórz nowe hasło",
|
||||
"Update Password": "Zaktualizuj hasło",
|
||||
"Disable Auth": "Wyłącz autoryzację",
|
||||
"Enable Auth": "Włącz autoryzację ",
|
||||
Logout: "Wyloguj się",
|
||||
Leave: "Zostaw",
|
||||
"I understand, please disable": "Rozumiem, proszę wyłączyć",
|
||||
Confirm: "Potwierdź",
|
||||
Yes: "Tak",
|
||||
No: "Nie",
|
||||
Username: "Nazwa użytkownika",
|
||||
Password: "Hasło",
|
||||
"Remember me": "Zapamiętaj mnie",
|
||||
Login: "Zaloguj się",
|
||||
"No Monitors, please": "Brak monitorów, proszę",
|
||||
"add one": "dodaj jeden",
|
||||
"Notification Type": "Typ powiadomienia",
|
||||
Email: "Email",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Informacje o certyfikacie",
|
||||
"Resolver Server": "Server resolver",
|
||||
"Resource Record Type": "Typ rekordu zasobów",
|
||||
"Last Result": "Ostatni wynik",
|
||||
"Create your admin account": "Utwórz swoje konto administratora",
|
||||
"Repeat Password": "Powtórz hasło",
|
||||
respTime: "Czas odp. (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Stwórz",
|
||||
clearEventsMsg: "Jesteś pewien, że chcesz usunąć wszystkie monitory dla tej strony?",
|
||||
clearHeartbeatsMsg: "Jesteś pewien, że chcesz usunąć wszystkie bicia serca dla tego monitora?",
|
||||
confirmClearStatisticsMsg: "Jesteś pewien, że chcesz usunąć WSZYSTKIE statystyki?",
|
||||
"Clear Data": "Usuń dane",
|
||||
Events: "Wydarzenia",
|
||||
Heartbeats: "Bicia serca",
|
||||
"Auto Get": "Pobierz automatycznie",
|
||||
enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.",
|
||||
"Default enabled": "Domyślnie włączone",
|
||||
"Also apply to existing monitors": "Również zastosuj do obecnych monitorów",
|
||||
Export: "Eksportuj",
|
||||
Import: "Importuj",
|
||||
backupDescription: "Możesz wykonać kopię zapasową wszystkich monitorów i wszystkich powiadomień do pliku JSON.",
|
||||
backupDescription2: "PS: Historia i dane zdarzeń nie są uwzględniane.",
|
||||
backupDescription3: "Poufne dane, takie jak tokeny powiadomień, są zawarte w pliku eksportu, prosimy o ostrożne przechowywanie.",
|
||||
alertNoFile: "Proszę wybrać plik do importu.",
|
||||
alertWrongFileType: "Proszę wybrać plik JSON.",
|
||||
twoFAVerifyLabel: "Proszę podaj swój token 2FA, aby sprawdzić czy 2FA działa",
|
||||
tokenValidSettingsMsg: "Token jest poprawny! Możesz teraz zapisać ustawienia 2FA.",
|
||||
confirmEnableTwoFAMsg: "Jesteś pewien że chcesz włączyć 2FA?",
|
||||
confirmDisableTwoFAMsg: "Jesteś pewien że chcesz wyłączyć 2FA?",
|
||||
"Apply on all existing monitors": "Zastosuj do wszystki obecnych monitorów",
|
||||
"Verify Token": "Weryfikuj token",
|
||||
"Setup 2FA": "Konfiguracja 2FA",
|
||||
"Enable 2FA": "Włącz 2FA",
|
||||
"Disable 2FA": "Wyłącz 2FA",
|
||||
"2FA Settings": "Ustawienia 2FA",
|
||||
"Two Factor Authentication": "Uwierzytelnienie dwuskładnikowe",
|
||||
Active: "Włączone",
|
||||
Inactive: "Wyłączone",
|
||||
Token: "Token",
|
||||
"Show URI": "Pokaż URI",
|
||||
"Clear all statistics": "Wyczyść wszystkie statystyki",
|
||||
retryCheckEverySecond: "Ponawiaj co {0} sekund.",
|
||||
importHandleDescription: "Wybierz 'Pomiń istniejące', jeśli chcesz pominąć każdy monitor lub powiadomienie o tej samej nazwie. 'Nadpisz' spowoduje usunięcie każdego istniejącego monitora i powiadomienia.",
|
||||
confirmImportMsg: "Czy na pewno chcesz zaimportować kopię zapasową? Upewnij się, że wybrałeś właściwą opcję importu.",
|
||||
"Heartbeat Retry Interval": "Częstotliwość ponawiania bicia serca",
|
||||
"Import Backup": "Importuj kopię zapasową",
|
||||
"Export Backup": "Eksportuj kopię zapasową",
|
||||
"Skip existing": "Pomiń istniejące",
|
||||
Overwrite: "Nadpisz",
|
||||
Options: "Opcje",
|
||||
"Keep both": "Zachowaj oba",
|
||||
Tags: "Tagi",
|
||||
"Add New below or Select...": "Dodaj nowy poniżej lub wybierz...",
|
||||
"Tag with this name already exist.": "Tag o tej nazwie już istnieje.",
|
||||
"Tag with this value already exist.": "Tag o tej wartości już istnieje.",
|
||||
color: "kolor",
|
||||
"value (optional)": "wartość (opcjonalnie)",
|
||||
Gray: "Szary",
|
||||
Red: "Czerwony",
|
||||
Orange: "Pomarańczowy",
|
||||
Green: "Zielony",
|
||||
Blue: "Niebieski",
|
||||
Indigo: "Indygo",
|
||||
Purple: "Fioletowy",
|
||||
Pink: "Różowy",
|
||||
"Search...": "Szukaj...",
|
||||
"Avg. Ping": "Średni ping",
|
||||
"Avg. Response": "Średnia odpowiedź",
|
||||
"Entry Page": "Wejdź na stronę",
|
||||
"statusPageNothing": "Nic tu nie ma, dodaj monitor lub grupę.",
|
||||
"No Services": "Brak usług",
|
||||
"All Systems Operational": "Wszystkie systemy działają",
|
||||
"Partially Degraded Service": "Częściowy błąd usługi",
|
||||
"Degraded Service": "Błąd usługi",
|
||||
"Add Group": "Dodaj grupę",
|
||||
"Add a monitor": "Dodaj monitoe",
|
||||
"Edit Status Page": "Edytuj stronę statusu",
|
||||
"Go to Dashboard": "Idź do panelu",
|
||||
};
|
182
src/languages/pt-BR.js
Normal file
182
src/languages/pt-BR.js
Normal file
@@ -0,0 +1,182 @@
|
||||
export default {
|
||||
languageName: "Português (Brasileiro)",
|
||||
checkEverySecond: "Verificar cada {0} segundos.",
|
||||
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
|
||||
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
|
||||
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
|
||||
upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
|
||||
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.",
|
||||
acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.",
|
||||
passwordNotMatchMsg: "A senha repetida não corresponde.",
|
||||
notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.",
|
||||
keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas",
|
||||
pauseDashboardHome: "Pausar",
|
||||
deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?",
|
||||
deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?",
|
||||
resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.",
|
||||
rrtypeDescription: "Selecione o RR-Type que você deseja monitorar",
|
||||
pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?",
|
||||
enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.",
|
||||
clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?",
|
||||
clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?",
|
||||
confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?",
|
||||
importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
|
||||
confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.",
|
||||
twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando",
|
||||
tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.",
|
||||
confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?",
|
||||
confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?",
|
||||
Settings: "Configurações",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Nova Atualização",
|
||||
Language: "Linguagem",
|
||||
Appearance: "Aparência",
|
||||
Theme: "Tema",
|
||||
General: "Geral",
|
||||
Version: "Versão",
|
||||
"Check Update On GitHub": "Verificar atualização no Github",
|
||||
List: "Lista",
|
||||
Add: "Adicionar",
|
||||
"Add New Monitor": "Adicionar novo monitor",
|
||||
"Quick Stats": "Estatísticas rápidas",
|
||||
Up: "On",
|
||||
Down: "Off",
|
||||
Pending: "Pendente",
|
||||
Unknown: "Desconhecido",
|
||||
Pause: "Pausar",
|
||||
Name: "Nome",
|
||||
Status: "Status",
|
||||
DateTime: "Data hora",
|
||||
Message: "Mensagem",
|
||||
"No important events": "Nenhum evento importante",
|
||||
Resume: "Resumo",
|
||||
Edit: "Editar",
|
||||
Delete: "Deletar",
|
||||
Current: "Atual",
|
||||
Uptime: "Tempo de atividade",
|
||||
"Cert Exp.": "Cert Exp.",
|
||||
days: "dias",
|
||||
day: "dia",
|
||||
"-day": "-dia",
|
||||
hour: "hora",
|
||||
"-hour": "-hora",
|
||||
Response: "Resposta",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Tipo de Monitor",
|
||||
Keyword: "Palavra-Chave",
|
||||
"Friendly Name": "Nome Amigável",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Porta",
|
||||
"Heartbeat Interval": "Intervalo de Heartbeat",
|
||||
Retries: "Novas tentativas",
|
||||
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat",
|
||||
Advanced: "Avançado",
|
||||
"Upside Down Mode": "Modo de cabeça para baixo",
|
||||
"Max. Redirects": "Redirecionamento Máx.",
|
||||
"Accepted Status Codes": "Status Code Aceitáveis",
|
||||
Save: "Salvar",
|
||||
Notifications: "Notificações",
|
||||
"Not available, please setup.": "Não disponível, por favor configure.",
|
||||
"Setup Notification": "Configurar Notificação",
|
||||
Light: "Claro",
|
||||
Dark: "Escuro",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
|
||||
Normal: "Normal",
|
||||
Bottom: "Inferior",
|
||||
None: "Nenhum",
|
||||
Timezone: "Fuso horário",
|
||||
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
|
||||
"Allow indexing": "Permitir Indexação",
|
||||
"Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site",
|
||||
"Change Password": "Mudar senha",
|
||||
"Current Password": "Senha atual",
|
||||
"New Password": "Nova Senha",
|
||||
"Repeat New Password": "Repetir Nova Senha",
|
||||
"Update Password": "Atualizar Senha",
|
||||
"Disable Auth": "Desativar Autenticação",
|
||||
"Enable Auth": "Ativar Autenticação",
|
||||
Logout: "Deslogar",
|
||||
Leave: "Sair",
|
||||
"I understand, please disable": "Eu entendo, por favor desative.",
|
||||
Confirm: "Confirmar",
|
||||
Yes: "Sim",
|
||||
No: "Não",
|
||||
Username: "Usuário",
|
||||
Password: "Senha",
|
||||
"Remember me": "Lembre-me",
|
||||
Login: "Autenticar",
|
||||
"No Monitors, please": "Nenhum monitor, por favor",
|
||||
"add one": "adicionar um",
|
||||
"Notification Type": "Tipo de Notificação",
|
||||
Email: "Email",
|
||||
Test: "Testar",
|
||||
"Certificate Info": "Info. do Certificado ",
|
||||
"Resolver Server": "Resolver Servidor",
|
||||
"Resource Record Type": "Tipo de registro de aplicação",
|
||||
"Last Result": "Último resultado",
|
||||
"Create your admin account": "Crie sua conta de admin",
|
||||
"Repeat Password": "Repita a senha",
|
||||
"Import Backup": "Importar Backup",
|
||||
"Export Backup": "Exportar Backup",
|
||||
Export: "Exportar",
|
||||
Import: "Importar",
|
||||
respTime: "Tempo de Resp. (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "Padrão habilitado",
|
||||
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
|
||||
Create: "Criar",
|
||||
"Clear Data": "Limpar Dados",
|
||||
Events: "Eventos",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Obter Automático",
|
||||
backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.",
|
||||
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
|
||||
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.",
|
||||
alertNoFile: "Selecione um arquivo para importar.",
|
||||
alertWrongFileType: "Selecione um arquivo JSON.",
|
||||
"Clear all statistics": "Limpar todas as estatísticas",
|
||||
"Skip existing": "Pular existente",
|
||||
Overwrite: "Sobrescrever",
|
||||
Options: "Opções",
|
||||
"Keep both": "Manter os dois",
|
||||
"Verify Token": "Verificar Token",
|
||||
"Setup 2FA": "Configurar 2FA",
|
||||
"Enable 2FA": "Ativar 2FA",
|
||||
"Disable 2FA": "Desativar 2FA",
|
||||
"2FA Settings": "Configurações do 2FA ",
|
||||
"Two Factor Authentication": "Autenticação e Dois Fatores",
|
||||
Active: "Ativo",
|
||||
Inactive: "Inativo",
|
||||
Token: "Token",
|
||||
"Show URI": "Mostrar URI",
|
||||
Tags: "Tag",
|
||||
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
|
||||
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
|
||||
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
|
||||
color: "cor",
|
||||
"value (optional)": "valor (opcional)",
|
||||
Gray: "Cinza",
|
||||
Red: "Vermelho",
|
||||
Orange: "Laranja",
|
||||
Green: "Verde",
|
||||
Blue: "Azul",
|
||||
Indigo: "Índigo",
|
||||
Purple: "Roxo",
|
||||
Pink: "Rosa",
|
||||
"Search...": "Buscar...",
|
||||
"Avg. Ping": "Ping Médio.",
|
||||
"Avg. Response": "Resposta Média. ",
|
||||
"Status Page": "Página de Status",
|
||||
"Entry Page": "Página de entrada",
|
||||
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
||||
"No Services": "Nenhum Serviço",
|
||||
"All Systems Operational": "Todos os Serviços Operacionais",
|
||||
"Partially Degraded Service": "Serviço parcialmente degradado",
|
||||
"Degraded Service": "Serviço Degradado",
|
||||
"Add Group": "Adicionar Grupo",
|
||||
"Add a monitor": "Adicionar um monitor",
|
||||
"Edit Status Page": "Editar Página de Status",
|
||||
"Go to Dashboard": "Ir para a dashboard",
|
||||
};
|
@@ -1,12 +1,11 @@
|
||||
export default {
|
||||
languageName: "Русский",
|
||||
checkEverySecond: "Проверять каждые {0} секунд.",
|
||||
"Avg.": "Средн. ",
|
||||
checkEverySecond: "проверять каждые {0} секунд",
|
||||
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
|
||||
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
|
||||
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
|
||||
maxRedirectDescription: "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
|
||||
acceptedStatusCodesDescription: "Выберите коды статусов, которые должны считаться за успешный ответ.",
|
||||
acceptedStatusCodesDescription: "Выберите коды статусов для определения доступности сервиса.",
|
||||
passwordNotMatchMsg: "Повтор пароля не совпадает.",
|
||||
notificationDescription: "Привяжите уведомления к мониторам.",
|
||||
keywordDescription: "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру)",
|
||||
@@ -17,7 +16,7 @@ export default {
|
||||
rrtypeDescription: "Выберите тип ресурсной записи, который вы хотите отслеживать",
|
||||
pauseMonitorMsg: "Вы действительно хотите поставить на паузу?",
|
||||
Settings: "Настройки",
|
||||
Dashboard: "Панель",
|
||||
Dashboard: "Панель мониторов",
|
||||
"New Update": "Обновление",
|
||||
Language: "Язык",
|
||||
Appearance: "Внешний вид",
|
||||
@@ -29,8 +28,8 @@ export default {
|
||||
Add: "Добавить",
|
||||
"Add New Monitor": "Новый монитор",
|
||||
"Quick Stats": "Статистика",
|
||||
Up: "Доступно",
|
||||
Down: "Недоступно",
|
||||
Up: "Доступен",
|
||||
Down: "Н/Д",
|
||||
Pending: "Ожидание",
|
||||
Unknown: "Неизвестно",
|
||||
Pause: "Пауза",
|
||||
@@ -62,7 +61,7 @@ export default {
|
||||
Retries: "Попыток",
|
||||
Advanced: "Дополнительно",
|
||||
"Upside Down Mode": "Режим реверса статуса",
|
||||
"Max. Redirects": "Макс. перенаправлений",
|
||||
"Max. Redirects": "Макс. количество перенаправлений",
|
||||
"Accepted Status Codes": "Допустимые коды статуса",
|
||||
Save: "Сохранить",
|
||||
Notifications: "Уведомления",
|
||||
@@ -107,6 +106,82 @@ export default {
|
||||
"Last Result": "Последний результат",
|
||||
"Create your admin account": "Создайте аккаунт администратора",
|
||||
"Repeat Password": "Повторите пароль",
|
||||
respTime: "Resp. Time (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
respTime: "Время ответа (мс)",
|
||||
notAvailableShort: "Н/Д",
|
||||
Create: "Создать",
|
||||
clearEventsMsg: "Вы действительно хотите удалить всю статистику событий данного монитора?",
|
||||
clearHeartbeatsMsg: "Вы действительно хотите удалить всю статистику опросов данного монитора?",
|
||||
confirmClearStatisticsMsg: "Вы действительно хотите удалить ВСЮ статистику?",
|
||||
"Clear Data": "Удалить статистику",
|
||||
Events: "События",
|
||||
Heartbeats: "Опросы",
|
||||
"Auto Get": "Авто-получение",
|
||||
enableDefaultNotificationDescription: "Для каждого нового монитора это уведомление будет включено по умолчанию. Вы всё ещё можете отключить уведомления в каждом мониторе отдельно.",
|
||||
"Default enabled": "Использовать по умолчанию",
|
||||
"Also apply to existing monitors": "Применить к существующим мониторам",
|
||||
Export: "Резервная копия",
|
||||
Import: "Восстановление",
|
||||
backupDescription: "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла",
|
||||
backupDescription2: "P.S. История и события сохранены не будут",
|
||||
backupDescription3: "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте",
|
||||
alertNoFile: "Выберите файл для импорта.",
|
||||
alertWrongFileType: "Выберите JSON-файл.",
|
||||
twoFAVerifyLabel: "Пожалуйста, введите свой токен, чтобы проверить работу 2FA",
|
||||
tokenValidSettingsMsg: "Токен действителен! Теперь вы можете сохранить настройки 2FA.",
|
||||
confirmEnableTwoFAMsg: "Вы действительно хотите включить 2FA?",
|
||||
confirmDisableTwoFAMsg: "Вы действительно хотите выключить 2FA?",
|
||||
"Apply on all existing monitors": "Применить ко всем существующим мониторам",
|
||||
"Verify Token": "Проверить токен",
|
||||
"Setup 2FA": "Настройка 2FA",
|
||||
"Enable 2FA": "Включить 2FA",
|
||||
"Disable 2FA": "Выключить 2FA",
|
||||
"2FA Settings": "Настройки 2FA",
|
||||
"Two Factor Authentication": "Двухфакторная аутентификация",
|
||||
Active: "Активно",
|
||||
Inactive: "Неактивно",
|
||||
Token: "Токен",
|
||||
"Show URI": "Показать URI",
|
||||
"Clear all statistics": "Удалить всю статистику",
|
||||
retryCheckEverySecond: "повторять каждые {0} секунд",
|
||||
importHandleDescription: "Выберите \"Пропустить существующие\", если вы хотите пропустить каждый монитор или уведомление с таким же именем. \"Перезаписать\" удалит каждый существующий монитор или уведомление и добавит заново. Вариант \"Не проверять\" принудительно восстанавливает все мониторы и уведомления, даже если они уже существуют.",
|
||||
confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.",
|
||||
"Heartbeat Retry Interval": "Интервал повтора опроса",
|
||||
"Import Backup": "Восстановление резервной копии",
|
||||
"Export Backup": "Резервная копия",
|
||||
"Skip existing": "Пропустить существующие",
|
||||
Overwrite: "Перезаписать",
|
||||
Options: "Опции",
|
||||
"Keep both": "Не проверять",
|
||||
Tags: "Теги",
|
||||
"Add New below or Select...": "Добавить новый или выбрать...",
|
||||
"Tag with this name already exist.": "Такой тег уже существует.",
|
||||
"Tag with this value already exist.": "Тег с таким значением уже существует.",
|
||||
color: "цвет",
|
||||
"value (optional)": "значение (опционально)",
|
||||
Gray: "Серый",
|
||||
Red: "Красный",
|
||||
Orange: "Оранжевый",
|
||||
Green: "Зелёный",
|
||||
Blue: "Синий",
|
||||
Indigo: "Индиго",
|
||||
Purple: "Пурпурный",
|
||||
Pink: "Розовый",
|
||||
"Search...": "Поиск...",
|
||||
"Avg. Ping": "Средн. пинг",
|
||||
"Avg. Response": "Средн. ответ",
|
||||
"Entry Page": "Главная страница",
|
||||
statusPageNothing: "Здесь пусто. Добавьте группу или монитор.",
|
||||
"No Services": "Нет сервисов",
|
||||
"All Systems Operational": "Все сервисы работают",
|
||||
"Partially Degraded Service": "Сервисы частично не работают",
|
||||
"Degraded Service": "Все сервисы не работают",
|
||||
"Add Group": "Добавить группу",
|
||||
"Add a monitor": "Добавить монитор",
|
||||
"Edit Status Page": "Редактировать",
|
||||
"Go to Dashboard": "Панель мониторов",
|
||||
"Status Page": "Статус сервисов",
|
||||
"Discard": "Отмена",
|
||||
"Create Incident": "Создать инцидент",
|
||||
"Switch to Dark Theme": "Тёмная тема",
|
||||
"Switch to Light Theme": "Светлая тема",
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "Srpski",
|
||||
checkEverySecond: "Proveri svakih {0} sekundi.",
|
||||
"Avg.": "Prosečni ",
|
||||
retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
|
||||
ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
|
||||
upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "Naprivi administratorski nalog",
|
||||
"Repeat Password": "Ponovite lozinku",
|
||||
respTime: "Vreme odg. (ms)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
languageName: "Српски",
|
||||
checkEverySecond: "Провери сваких {0} секунди.",
|
||||
"Avg.": "Просечни ",
|
||||
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
|
||||
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
|
||||
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
|
||||
@@ -108,5 +107,76 @@ export default {
|
||||
"Create your admin account": "Наприви администраторски налог",
|
||||
"Repeat Password": "Поновите лозинку",
|
||||
respTime: "Време одг. (мс)",
|
||||
notAvailableShort: "N/A"
|
||||
}
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user