mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 07:26:59 +08:00
Compare commits
164 Commits
1.22.1
...
1.23.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
18a2a8eb1e | ||
|
e9585ccbf4 | ||
|
5e39b0daf6 | ||
|
71fca3f0c3 | ||
|
2921f33c24 | ||
|
4e0bb394db | ||
|
ced576feba | ||
|
ceb5708bfd | ||
|
23fdd32de0 | ||
|
36777c5eff | ||
|
439b6517d1 | ||
|
c6e68fac97 | ||
|
bce4835362 | ||
|
a032e11a2e | ||
|
d231a05526 | ||
|
f4967615c0 | ||
|
67b1974718 | ||
|
062e9db2a7 | ||
|
90e1b4cf56 | ||
|
43698b23c6 | ||
|
f19b27c1c7 | ||
|
42b5d30a33 | ||
|
cda77c1a32 | ||
|
157cf47d38 | ||
|
09c6798a30 | ||
|
587d9e4781 | ||
|
ae2867e305 | ||
|
65ffd77d35 | ||
|
9dd652733e | ||
|
0a59fef7d8 | ||
|
27ce47277b | ||
|
7f68e4a987 | ||
|
149d08ecbb | ||
|
eb6167aaf1 | ||
|
db66195f7d | ||
|
59245e624d | ||
|
c0eb0cb42c | ||
|
573f158f7f | ||
|
6413d4cbdf | ||
|
cf5a04bc5e | ||
|
a0203372ce | ||
|
5ccf2d23fc | ||
|
bf68e0a7bc | ||
|
6984596568 | ||
|
f3562864ee | ||
|
9058a829a1 | ||
|
0684313ec9 | ||
|
d7e12dc92d | ||
|
1d9a28e9ab | ||
|
ddbf367011 | ||
|
50d4091ded | ||
|
66cfbd02c3 | ||
|
688f23035b | ||
|
7701e2ad36 | ||
|
8e72d6f534 | ||
|
278b88a9d9 | ||
|
084cf01fcd | ||
|
f6c1b92fc6 | ||
|
25c8196641 | ||
|
baf5613dfa | ||
|
695691468c | ||
|
4891ec4527 | ||
|
e2a87eb430 | ||
|
80927332cb | ||
|
a0eb733d54 | ||
|
21d556528f | ||
|
357466cc90 | ||
|
b038d09349 | ||
|
5dd4231e56 | ||
|
c6d0c431bd | ||
|
d1b7f4c834 | ||
|
5c4180fb45 | ||
|
345e61abca | ||
|
dd1526deff | ||
|
be26bb75d9 | ||
|
99fb5836e2 | ||
|
2f5a565ce4 | ||
|
973db9d4b2 | ||
|
2e75142fe5 | ||
|
ad3ffacf45 | ||
|
6f4af30701 | ||
|
b1f266ceb1 | ||
|
6bece8796e | ||
|
e7d1b4e14a | ||
|
e5c6783781 | ||
|
ac68a35d3a | ||
|
d825dbf828 | ||
|
cfb4bbc6cb | ||
|
293015ff35 | ||
|
18d8b3a8e0 | ||
|
d55794e1a5 | ||
|
3d50572dd7 | ||
|
cdb38d49eb | ||
|
80b55786a4 | ||
|
fe40d819bd | ||
|
3dbd8277f0 | ||
|
771d21c4ad | ||
|
ed6b4e5ae5 | ||
|
3b9c95a8a8 | ||
|
cdf6922bdd | ||
|
9954ba82e7 | ||
|
19873e5b9e | ||
|
13ae878ee8 | ||
|
4ea5771f97 | ||
|
1774bb86dc | ||
|
c583037dff | ||
|
9da712054a | ||
|
af78da1dd9 | ||
|
9e041f219b | ||
|
8c60e902e1 | ||
|
de74efb2e6 | ||
|
9ee2780e9e | ||
|
a386f1fc9e | ||
|
35154ef9c5 | ||
|
1baa592824 | ||
|
9882fc65b1 | ||
|
3e5e7e6e32 | ||
|
0e725569e5 | ||
|
afcfb7e19c | ||
|
eaee55fc8f | ||
|
affac0a97b | ||
|
a12e7eba72 | ||
|
4f6035899d | ||
|
dd77baabe1 | ||
|
820f2eec9f | ||
|
f8c9a20afd | ||
|
cea894cc6d | ||
|
79b38e0e7b | ||
|
7cc9783436 | ||
|
8955c3816b | ||
|
7761e9a05e | ||
|
c9d6e576ab | ||
|
97d38ee1a8 | ||
|
bdcbd6389b | ||
|
a00561ff09 | ||
|
6708eed121 | ||
|
3c56a6f395 | ||
|
d37c33ad42 | ||
|
8897385690 | ||
|
6132a45c7c | ||
|
f68452c47a | ||
|
9d71e34a83 | ||
|
1fa8c0f9fe | ||
|
9a8bea5761 | ||
|
376d84c742 | ||
|
f3fe392ec4 | ||
|
9c3bb67b6b | ||
|
5200e10aab | ||
|
f1a396b0f7 | ||
|
9825b33ef3 | ||
|
00f733d352 | ||
|
fd10897988 | ||
|
317024ed72 | ||
|
f604d96c5b | ||
|
f30f00655f | ||
|
891f09def7 | ||
|
6b5e179bb0 | ||
|
f653aba735 | ||
|
682f8e52a8 | ||
|
171aff1226 | ||
|
bb7de6aa88 | ||
|
6de0c6a90c | ||
|
94b69935fe | ||
|
3f30feaefb |
28
.devcontainer/README.md
Normal file
28
.devcontainer/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Codespaces
|
||||||
|
|
||||||
|
You can modifiy Uptime Kuma in your browser without setting up a local development.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. Click `Code` -> `Create codespace on master`
|
||||||
|
2. Wait a few minutes until you see there are two exposed ports
|
||||||
|
3. Go to the `3000` url, see if it is working
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
|
||||||
|
You don't need to restart the frontend, unless you try to add a new frontend dependency.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
The backend does not automatically hot-reload.
|
||||||
|
You will need to restart the backend after changing something using these steps:
|
||||||
|
|
||||||
|
1. Click `Terminal`
|
||||||
|
2. Click `Codespaces: server-dev` in the right panel
|
||||||
|
3. Press `Ctrl + C` to stop the server
|
||||||
|
4. Press `Up` to run `npm run start-server-dev`
|
||||||
|
|
||||||
|

|
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
"updateContentCommand": "npm ci",
|
||||||
|
"postCreateCommand": "",
|
||||||
|
"postAttachCommand": {
|
||||||
|
"frontend-dev": "npm run start-frontend-devcontainer",
|
||||||
|
"server-dev": "npm run start-server-dev",
|
||||||
|
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [3000, 3001]
|
||||||
|
}
|
@@ -18,6 +18,7 @@ README.md
|
|||||||
.vscode
|
.vscode
|
||||||
.eslint*
|
.eslint*
|
||||||
.stylelint*
|
.stylelint*
|
||||||
|
/.devcontainer
|
||||||
/.github
|
/.github
|
||||||
yarn.lock
|
yarn.lock
|
||||||
app.json
|
app.json
|
||||||
@@ -35,6 +36,7 @@ tsconfig.json
|
|||||||
/extra/healthcheck
|
/extra/healthcheck
|
||||||
extra/exe-builder
|
extra/exe-builder
|
||||||
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
#node_modules
|
#node_modules
|
||||||
|
4
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
4
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -44,7 +44,7 @@ body:
|
|||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: "💻 Operating System and Arch"
|
label: "💻 Operating System and Arch"
|
||||||
description: "Which OS is your server/device running on?"
|
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
||||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -52,7 +52,7 @@ body:
|
|||||||
id: browser-vendor
|
id: browser-vendor
|
||||||
attributes:
|
attributes:
|
||||||
label: "🌐 Browser"
|
label: "🌐 Browser"
|
||||||
description: "Which browser are you running on?"
|
description: "Which browser are you running on? (For Replit, please do not report this bug)"
|
||||||
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
name: Auto Test
|
name: Auto Test
|
||||||
@@ -21,8 +21,8 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||||
node: [ 14, 16, 18, 20 ]
|
node: [ 14, 20 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: 'npm'
|
- run: npm install npm@latest -g
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
@@ -41,6 +41,29 @@ jobs:
|
|||||||
HEADLESS_TEST: 1
|
HEADLESS_TEST: 1
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||||
|
|
||||||
|
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||||
|
armv7-simple-test:
|
||||||
|
needs: [ check-linters ]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ARMv7 ]
|
||||||
|
node: [ 14.21.3, 20.5.0 ]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
- run: npm install npm@latest -g
|
||||||
|
- run: npm ci --production
|
||||||
|
|
||||||
check-linters:
|
check-linters:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -52,7 +75,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
cache: 'npm'
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
|
||||||
@@ -67,7 +89,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
cache: 'npm'
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:test
|
- run: npm run cy:test
|
||||||
@@ -83,7 +104,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
cache: 'npm'
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:run:unit
|
- run: npm run cy:run:unit
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
"color-function-notation": "legacy",
|
"color-function-notation": "legacy",
|
||||||
"shorthand-property-no-redundant-values": null,
|
"shorthand-property-no-redundant-values": null,
|
||||||
"color-hex-length": null,
|
"color-hex-length": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
|
"at-rule-no-unknown": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,19 +34,19 @@ Yes or no, it depends on what you will try to do. Since I don't want to waste yo
|
|||||||
|
|
||||||
Here are some references:
|
Here are some references:
|
||||||
|
|
||||||
✅ Usually Accept:
|
### ✅ Usually accepted:
|
||||||
- Bug fix
|
- Bug fix
|
||||||
- Security fix
|
- Security fix
|
||||||
- Adding notification providers
|
- Adding notification providers
|
||||||
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
|
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
||||||
- Adding new language keys: `$t("...")`
|
- Adding new language keys: `$t("...")`
|
||||||
|
|
||||||
⚠️ Discussion First
|
### ⚠️ Discussion required:
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New features
|
- New features
|
||||||
|
|
||||||
❌ Won't Merge
|
### ❌ Won't be merged:
|
||||||
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
|
- A dedicated pr for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
||||||
- Do not pass the auto test
|
- Do not pass the auto test
|
||||||
- Any breaking changes
|
- Any breaking changes
|
||||||
- Duplicated pull requests
|
- Duplicated pull requests
|
||||||
@@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- Node.js >= 14
|
- [`Node.js`](https://nodejs.org/) >= 14
|
||||||
- NPM >= 8.5
|
- [`npm`](https://www.npmjs.com/) >= 8.5
|
||||||
- Git
|
- [`git`](https://git-scm.com/)
|
||||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
||||||
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
||||||
|
|
||||||
## Install Dependencies for Development
|
## Install Dependencies for Development
|
||||||
|
|
||||||
@@ -218,7 +218,17 @@ If for maybe security reasons, a library must be updated. Then you must need to
|
|||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are ommited, they can not be translated).
|
||||||
|
|
||||||
|
**Don't include any other languages in your inital Pull-Request** (even if this is your mother tounge), to avoid merge-conflicts between weblate and `master`.
|
||||||
|
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language-skills.
|
||||||
|
|
||||||
|
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||||
|
|
||||||
|
## Spelling & Grammar
|
||||||
|
|
||||||
|
Feel free to correct the grammar in the documentation or code.
|
||||||
|
My mother language is not english and my grammar is not that great.
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
|
31
README.md
31
README.md
@@ -1,16 +1,16 @@
|
|||||||
|
<div align="center" width="100%">
|
||||||
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
# Uptime Kuma
|
# Uptime Kuma
|
||||||
|
|
||||||
|
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||||
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
||||||
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center" width="100%">
|
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
||||||
|
|
||||||
## 🥔 Live Demo
|
## 🥔 Live Demo
|
||||||
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
|||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||||
* Fancy, Reactive, Fast UI/UX
|
* Fancy, Reactive, Fast UI/UX
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
||||||
* 20 second intervals
|
* 20 second intervals
|
||||||
@@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
|
|||||||
|
|
||||||
### 💪🏻 Non-Docker
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- Platform
|
- Platform
|
||||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
||||||
- ❌ Replit / Heroku
|
- ❌ Replit / Heroku
|
||||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported)
|
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
|
||||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
- [npm](https://docs.npmjs.com/cli/) >= 7
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -71,7 +71,7 @@ npm run setup
|
|||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (Recommended) Option 2. Run in background using PM2
|
||||||
# Install PM2 if you don't have it:
|
# Install PM2 if you don't have it:
|
||||||
npm install pm2 -g && pm2 install pm2-logrotate
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
|
|
||||||
# Start Server
|
# Start Server
|
||||||
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
|
|||||||
|
|
||||||
### Windows Portable (x64)
|
### Windows Portable (x64)
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/releases/download/1.21.0/uptime-kuma-win64-portable-1.0.0.zip
|
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
@@ -184,7 +184,10 @@ If you want to report a bug or request a new feature, feel free to open a [new i
|
|||||||
### Translations
|
### Translations
|
||||||
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||||
|
|
||||||
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
## Spelling & Grammar
|
||||||
|
|
||||||
|
Feel free to correct the grammar in the documentation or code.
|
||||||
|
My mother language is not english and my grammar is not that great.
|
||||||
|
|
||||||
### Create Pull Requests
|
### Create Pull Requests
|
||||||
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
@@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
|
|||||||
config.presets = [ "@babel/preset-env" ];
|
config.presets = [ "@babel/preset-env" ];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.TEST_BACKEND) {
|
|
||||||
config.plugins = [ "babel-plugin-rewire" ];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import visualizer from "rollup-plugin-visualizer";
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
import commonjs from "vite-plugin-commonjs";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
@@ -16,8 +17,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
|
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
||||||
|
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
||||||
|
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
commonjs(),
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: [ "since 2015" ],
|
targets: [ "since 2015" ],
|
||||||
@@ -42,6 +47,9 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [ /.js$/ ],
|
||||||
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
7
db/patch-add-certificate-expiry-status-page.sql
Normal file
7
db/patch-add-certificate-expiry-status-page.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 status_page
|
||||||
|
ADD show_certificate_expiry BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-add-gamedig-given-port.sql
Normal file
7
db/patch-add-gamedig-given-port.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 gamedig_given_port_only BOOLEAN default 1 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-add-invert-keyword.sql
Normal file
7
db/patch-add-invert-keyword.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 invert_keyword BOOLEAN default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
6
db/patch-add-timeout-monitor.sql
Normal file
6
db/patch-add-timeout-monitor.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD timeout DOUBLE default 0 not null;
|
||||||
|
COMMIT;
|
10
db/patch-added-json-query.sql
Normal file
10
db/patch-added-json-query.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD json_path TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD expected_value VARCHAR(255);
|
||||||
|
|
||||||
|
COMMIT;
|
22
db/patch-added-kafka-producer.sql
Normal file
22
db/patch-added-kafka-producer.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;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_topic VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_brokers TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_ssl INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_sasl_options TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD kafka_producer_message TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
19
db/patch-monitor-oauth-cc.sql
Normal file
19
db/patch-monitor-oauth-cc.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.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD oauth_client_id TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD oauth_client_secret TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD oauth_token_url TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD oauth_scopes TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD oauth_auth_method TEXT default null;
|
||||||
|
|
||||||
|
COMMIT;
|
@@ -5,13 +5,29 @@ ARG TARGETPLATFORM
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Curl
|
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# python3* = apprise's dependencies
|
||||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
# sqlite3 = for debugging
|
||||||
|
# iputils-ping = for ping
|
||||||
|
# util-linux = for setpriv (Should be dropped in 2.0.0?)
|
||||||
|
# dumb-init = avoid zombie processes (#480)
|
||||||
|
# curl = for debugging
|
||||||
|
# ca-certificates = keep the cert up-to-date
|
||||||
|
# sudo = for start service nscd with non-root user
|
||||||
|
# nscd = for better DNS caching
|
||||||
|
# (pip) apprise = for notifications
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
apt-get --yes --no-install-recommends install \
|
||||||
sqlite3 iputils-ping util-linux dumb-init git curl ca-certificates && \
|
python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
sqlite3 \
|
||||||
|
iputils-ping \
|
||||||
|
util-linux \
|
||||||
|
dumb-init \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
sudo \
|
||||||
|
nscd && \
|
||||||
|
pip3 --no-cache-dir install apprise==1.4.5 && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
@@ -26,3 +42,7 @@ RUN set -eux && \
|
|||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
|
# For nscd
|
||||||
|
COPY ./docker/etc/nscd.conf /etc/nscd.conf
|
||||||
|
COPY ./docker/etc/sudoers /etc/sudoers
|
||||||
|
|
||||||
|
@@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
|
|||||||
FROM louislam/uptime-kuma:base-debian AS release
|
FROM louislam/uptime-kuma:base-debian AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV UPTIME_KUMA_IS_CONTAINER=1
|
||||||
|
|
||||||
# Copy app files from build layer
|
# Copy app files from build layer
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
@@ -70,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
EXPOSE 3000 3001
|
EXPOSE 3000 3001
|
||||||
VOLUME ["/app/data"]
|
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
||||||
CMD ["npm", "run", "start-pr-test"]
|
CMD ["npm", "run", "start-pr-test"]
|
||||||
|
|
||||||
|
90
docker/etc/nscd.conf
Normal file
90
docker/etc/nscd.conf
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#
|
||||||
|
# /etc/nscd.conf
|
||||||
|
#
|
||||||
|
# An example Name Service Cache config file. This file is needed by nscd.
|
||||||
|
#
|
||||||
|
# Legal entries are:
|
||||||
|
#
|
||||||
|
# logfile <file>
|
||||||
|
# debug-level <level>
|
||||||
|
# threads <initial #threads to use>
|
||||||
|
# max-threads <maximum #threads to use>
|
||||||
|
# server-user <user to run server as instead of root>
|
||||||
|
# server-user is ignored if nscd is started with -S parameters
|
||||||
|
# stat-user <user who is allowed to request statistics>
|
||||||
|
# reload-count unlimited|<number>
|
||||||
|
# paranoia <yes|no>
|
||||||
|
# restart-interval <time in seconds>
|
||||||
|
#
|
||||||
|
# enable-cache <service> <yes|no>
|
||||||
|
# positive-time-to-live <service> <time in seconds>
|
||||||
|
# negative-time-to-live <service> <time in seconds>
|
||||||
|
# suggested-size <service> <prime number>
|
||||||
|
# check-files <service> <yes|no>
|
||||||
|
# persistent <service> <yes|no>
|
||||||
|
# shared <service> <yes|no>
|
||||||
|
# max-db-size <service> <number bytes>
|
||||||
|
# auto-propagate <service> <yes|no>
|
||||||
|
#
|
||||||
|
# Currently supported cache names (services): passwd, group, hosts, services
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# logfile /var/log/nscd.log
|
||||||
|
# threads 4
|
||||||
|
# max-threads 32
|
||||||
|
# server-user node
|
||||||
|
# stat-user somebody
|
||||||
|
debug-level 0
|
||||||
|
# reload-count 5
|
||||||
|
paranoia no
|
||||||
|
# restart-interval 3600
|
||||||
|
|
||||||
|
enable-cache passwd no
|
||||||
|
positive-time-to-live passwd 600
|
||||||
|
negative-time-to-live passwd 20
|
||||||
|
suggested-size passwd 211
|
||||||
|
check-files passwd yes
|
||||||
|
persistent passwd yes
|
||||||
|
shared passwd yes
|
||||||
|
max-db-size passwd 33554432
|
||||||
|
auto-propagate passwd yes
|
||||||
|
|
||||||
|
enable-cache group no
|
||||||
|
positive-time-to-live group 3600
|
||||||
|
negative-time-to-live group 60
|
||||||
|
suggested-size group 211
|
||||||
|
check-files group yes
|
||||||
|
persistent group yes
|
||||||
|
shared group yes
|
||||||
|
max-db-size group 33554432
|
||||||
|
auto-propagate group yes
|
||||||
|
|
||||||
|
enable-cache hosts yes
|
||||||
|
positive-time-to-live hosts 3600
|
||||||
|
negative-time-to-live hosts 20
|
||||||
|
suggested-size hosts 211
|
||||||
|
check-files hosts yes
|
||||||
|
persistent hosts yes
|
||||||
|
# Set shared to "no" to display stats in `nscd -g`
|
||||||
|
# Read more: https://stackoverflow.com/questions/40429245/nscdcentos7curl-0-dns-cache-hit-rate
|
||||||
|
shared hosts no
|
||||||
|
max-db-size hosts 33554432
|
||||||
|
|
||||||
|
enable-cache services no
|
||||||
|
positive-time-to-live services 28800
|
||||||
|
negative-time-to-live services 20
|
||||||
|
suggested-size services 211
|
||||||
|
check-files services yes
|
||||||
|
persistent services yes
|
||||||
|
shared services yes
|
||||||
|
max-db-size services 33554432
|
||||||
|
|
||||||
|
enable-cache netgroup no
|
||||||
|
positive-time-to-live netgroup 28800
|
||||||
|
negative-time-to-live netgroup 20
|
||||||
|
suggested-size netgroup 211
|
||||||
|
check-files netgroup yes
|
||||||
|
persistent netgroup yes
|
||||||
|
shared netgroup yes
|
||||||
|
max-db-size netgroup 33554432
|
31
docker/etc/sudoers
Normal file
31
docker/etc/sudoers
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# This file MUST be edited with the 'visudo' command as root.
|
||||||
|
#
|
||||||
|
# Please consider adding local content in /etc/sudoers.d/ instead of
|
||||||
|
# directly modifying this file.
|
||||||
|
#
|
||||||
|
# See the man page for details on how to write a sudoers file.
|
||||||
|
#
|
||||||
|
Defaults env_reset
|
||||||
|
Defaults mail_badpass
|
||||||
|
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
|
||||||
|
# Host alias specification
|
||||||
|
|
||||||
|
# User alias specification
|
||||||
|
|
||||||
|
# Cmnd alias specification
|
||||||
|
|
||||||
|
# User privilege specification
|
||||||
|
root ALL=(ALL:ALL) ALL
|
||||||
|
|
||||||
|
# Allow members of group sudo to execute any command
|
||||||
|
%sudo ALL=(ALL:ALL) ALL
|
||||||
|
|
||||||
|
# See sudoers(5) for more information on "#include" directives:
|
||||||
|
|
||||||
|
#includedir /etc/sudoers.d
|
||||||
|
|
||||||
|
# Allow `node` to control service (mainly for nscd)
|
||||||
|
node ALL=(root) NOPASSWD: /usr/sbin/nscdservice
|
||||||
|
node ALL=(root) NOPASSWD: /usr/sbin/service
|
@@ -1,3 +1,3 @@
|
|||||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||||
<Costura />
|
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
||||||
</Weavers>
|
</Weavers>
|
@@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
|
|||||||
// set of attributes. Change these attribute values to modify the information
|
// set of attributes. Change these attribute values to modify the information
|
||||||
// associated with an assembly.
|
// associated with an assembly.
|
||||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||||
[assembly: AssemblyDescription("")]
|
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("")]
|
[assembly: AssemblyCompany("Uptime Kuma")]
|
||||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
@@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
|
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
||||||
|
|
||||||
// Version information for an assembly consists of the following four values:
|
// Version information for an assembly consists of the following four values:
|
||||||
//
|
//
|
||||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
|||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("1.0.1.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.1.0")]
|
||||||
|
@@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||||
println("=====================");
|
println("=====================");
|
||||||
println("Uptime Kuma Installer");
|
println("Uptime Kuma Install Script");
|
||||||
println("=====================");
|
println("=====================");
|
||||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
|
||||||
println("---------------------------------------");
|
println("---------------------------------------");
|
||||||
println("This script is designed for Linux and basic usage.");
|
println("This script is designed for Linux and basic usage.");
|
||||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||||
println("---------------------------------------");
|
println("---------------------------------------");
|
||||||
println("");
|
println("");
|
||||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
|
||||||
println("Docker - Install Uptime Kuma Docker container");
|
println("Docker - Install Uptime Kuma Docker container");
|
||||||
println("");
|
println("");
|
||||||
|
|
||||||
@@ -29,14 +29,10 @@ function checkNode() {
|
|||||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||||
println("Node Version: " ++ nodeVersion);
|
println("Node Version: " ++ nodeVersion);
|
||||||
|
|
||||||
if (nodeVersion < "12") {
|
if (nodeVersion <= "12") {
|
||||||
println("Error: Required Node.js 14");
|
println("Error: Required Node.js 14");
|
||||||
call("exit", "1");
|
call("exit", "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeVersion == "12") {
|
|
||||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deb() {
|
function deb() {
|
||||||
@@ -60,8 +56,8 @@ function deb() {
|
|||||||
bash("apt --yes install curl");
|
bash("apt --yes install curl");
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Installing Node.js 14");
|
println("Installing Node.js 16");
|
||||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
|
||||||
bash("apt --yes install nodejs");
|
bash("apt --yes install nodejs");
|
||||||
bash("node -v");
|
bash("node -v");
|
||||||
|
|
||||||
@@ -91,6 +87,10 @@ if (type == "local") {
|
|||||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||||
if (os == "Ubuntu") {
|
if (os == "Ubuntu") {
|
||||||
distribution = "ubuntu";
|
distribution = "ubuntu";
|
||||||
|
|
||||||
|
// Get ubuntu version
|
||||||
|
bash(". /etc/lsb-release");
|
||||||
|
version = DISTRIB_RELEASE;
|
||||||
}
|
}
|
||||||
if (os == "Debian") {
|
if (os == "Debian") {
|
||||||
distribution = "debian";
|
distribution = "debian";
|
||||||
@@ -101,6 +101,7 @@ if (type == "local") {
|
|||||||
|
|
||||||
println("Your OS: " ++ os);
|
println("Your OS: " ++ os);
|
||||||
println("Distribution: " ++ distribution);
|
println("Distribution: " ++ distribution);
|
||||||
|
println("Version: " ++ version);
|
||||||
println("Arch: " ++ arch);
|
println("Arch: " ++ arch);
|
||||||
|
|
||||||
if ("$3" != "") {
|
if ("$3" != "") {
|
||||||
@@ -131,15 +132,32 @@ if (type == "local") {
|
|||||||
checkNode();
|
checkNode();
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
bash("curlCheck=$(curl --version)");
|
bash("dnfCheck=$(dnf --version)");
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
// Use yum
|
||||||
bash("yum -y -q install curl");
|
if (dnfCheck == "") {
|
||||||
|
bash("curlCheck=$(curl --version)");
|
||||||
|
if (curlCheck == "") {
|
||||||
|
println("Installing Curl");
|
||||||
|
bash("yum -y -q install curl");
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Installing Node.js 16");
|
||||||
|
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
||||||
|
bash("yum install -y -q nodejs");
|
||||||
|
} else {
|
||||||
|
bash("curlCheck=$(curl --version)");
|
||||||
|
if (curlCheck == "") {
|
||||||
|
println("Installing Curl");
|
||||||
|
bash("dnf -y install curl");
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Installing Node.js 16");
|
||||||
|
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
||||||
|
bash("dnf install -y nodejs");
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Installing Node.js 14");
|
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
|
||||||
bash("yum install -y -q nodejs");
|
|
||||||
bash("node -v");
|
bash("node -v");
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
bash("nodeCheckAgain=$(node -v)");
|
||||||
@@ -193,6 +211,14 @@ if (type == "local") {
|
|||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check again
|
||||||
|
bash("check=$(pm2 --version)");
|
||||||
|
if (check == "") {
|
||||||
|
println("Error: pm2 is not found!");
|
||||||
|
bash("exit 1");
|
||||||
|
}
|
||||||
|
|
||||||
bash("mkdir -p $installPath");
|
bash("mkdir -p $installPath");
|
||||||
bash("cd $installPath");
|
bash("cd $installPath");
|
||||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||||
|
54
install.sh
54
install.sh
@@ -3,15 +3,15 @@
|
|||||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||||
"echo" "-e" "====================="
|
"echo" "-e" "====================="
|
||||||
"echo" "-e" "Uptime Kuma Installer"
|
"echo" "-e" "Uptime Kuma Install Script"
|
||||||
"echo" "-e" "====================="
|
"echo" "-e" "====================="
|
||||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
|
||||||
"echo" "-e" "---------------------------------------"
|
"echo" "-e" "---------------------------------------"
|
||||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||||
"echo" "-e" "---------------------------------------"
|
"echo" "-e" "---------------------------------------"
|
||||||
"echo" "-e" ""
|
"echo" "-e" ""
|
||||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
|
||||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||||
"echo" "-e" ""
|
"echo" "-e" ""
|
||||||
if [ "$1" != "" ]; then
|
if [ "$1" != "" ]; then
|
||||||
@@ -25,12 +25,9 @@ function checkNode {
|
|||||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||||
_0="12"
|
_0="12"
|
||||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
if [ $(($nodeVersion <= $_0)) == 1 ]; then
|
||||||
"echo" "-e" "Error: Required Node.js 14"
|
"echo" "-e" "Error: Required Node.js 14"
|
||||||
"exit" "1"
|
"exit" "1"
|
||||||
fi
|
|
||||||
if [ "$nodeVersion" == "12" ]; then
|
|
||||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
function deb {
|
function deb {
|
||||||
@@ -50,8 +47,8 @@ fi
|
|||||||
"echo" "-e" "Installing Curl"
|
"echo" "-e" "Installing Curl"
|
||||||
apt --yes install curl
|
apt --yes install curl
|
||||||
fi
|
fi
|
||||||
"echo" "-e" "Installing Node.js 14"
|
"echo" "-e" "Installing Node.js 16"
|
||||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
|
||||||
apt --yes install nodejs
|
apt --yes install nodejs
|
||||||
node -v
|
node -v
|
||||||
nodeCheckAgain=$(node -v)
|
nodeCheckAgain=$(node -v)
|
||||||
@@ -75,7 +72,10 @@ if [ "$type" == "local" ]; then
|
|||||||
if [ -e "/etc/issue" ]; then
|
if [ -e "/etc/issue" ]; then
|
||||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||||
if [ "$os" == "Ubuntu" ]; then
|
if [ "$os" == "Ubuntu" ]; then
|
||||||
distribution="ubuntu"
|
distribution="ubuntu"
|
||||||
|
# Get ubuntu version
|
||||||
|
. /etc/lsb-release
|
||||||
|
version="$DISTRIB_RELEASE"
|
||||||
fi
|
fi
|
||||||
if [ "$os" == "Debian" ]; then
|
if [ "$os" == "Debian" ]; then
|
||||||
distribution="debian"
|
distribution="debian"
|
||||||
@@ -85,6 +85,7 @@ fi
|
|||||||
arch=$(uname -i)
|
arch=$(uname -i)
|
||||||
"echo" "-e" "Your OS: ""$os"
|
"echo" "-e" "Your OS: ""$os"
|
||||||
"echo" "-e" "Distribution: ""$distribution"
|
"echo" "-e" "Distribution: ""$distribution"
|
||||||
|
"echo" "-e" "Version: ""$version"
|
||||||
"echo" "-e" "Arch: ""$arch"
|
"echo" "-e" "Arch: ""$arch"
|
||||||
if [ "$3" != "" ]; then
|
if [ "$3" != "" ]; then
|
||||||
port="$3"
|
port="$3"
|
||||||
@@ -108,14 +109,27 @@ fi
|
|||||||
if [ "$nodeCheck" != "" ]; then
|
if [ "$nodeCheck" != "" ]; then
|
||||||
"checkNode"
|
"checkNode"
|
||||||
else
|
else
|
||||||
curlCheck=$(curl --version)
|
dnfCheck=$(dnf --version)
|
||||||
if [ "$curlCheck" == "" ]; then
|
# Use yum
|
||||||
"echo" "-e" "Installing Curl"
|
if [ "$dnfCheck" == "" ]; then
|
||||||
yum -y -q install curl
|
curlCheck=$(curl --version)
|
||||||
|
if [ "$curlCheck" == "" ]; then
|
||||||
|
"echo" "-e" "Installing Curl"
|
||||||
|
yum -y -q install curl
|
||||||
fi
|
fi
|
||||||
"echo" "-e" "Installing Node.js 14"
|
"echo" "-e" "Installing Node.js 16"
|
||||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
||||||
yum install -y -q nodejs
|
yum install -y -q nodejs
|
||||||
|
else
|
||||||
|
curlCheck=$(curl --version)
|
||||||
|
if [ "$curlCheck" == "" ]; then
|
||||||
|
"echo" "-e" "Installing Curl"
|
||||||
|
dnf -y install curl
|
||||||
|
fi
|
||||||
|
"echo" "-e" "Installing Node.js 16"
|
||||||
|
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
||||||
|
dnf install -y nodejs
|
||||||
|
fi
|
||||||
node -v
|
node -v
|
||||||
nodeCheckAgain=$(node -v)
|
nodeCheckAgain=$(node -v)
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
if [ "$nodeCheckAgain" == "" ]; then
|
||||||
@@ -161,6 +175,12 @@ fi
|
|||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g && pm2 install pm2-logrotate
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
|
fi
|
||||||
|
# Check again
|
||||||
|
check=$(pm2 --version)
|
||||||
|
if [ "$check" == "" ]; then
|
||||||
|
"echo" "-e" "Error: pm2 is not found!"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
cd $installPath
|
cd $installPath
|
||||||
|
5435
package-lock.json
generated
5435
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.22.1",
|
"version": "1.23.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.* || 16.* || 18.*"
|
"node": "14 || 16 || 18 || >= 20.4.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install",
|
"install-legacy": "npm install",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||||
|
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
@@ -45,11 +46,14 @@
|
|||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||||
|
"test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .",
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
|
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
|
||||||
|
"test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .",
|
||||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||||
|
"test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .",
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.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",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||||
@@ -69,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "~1.7.3",
|
"@grpc/grpc-js": "~1.7.3",
|
||||||
"@louislam/ping": "~0.4.4-mod.0",
|
"@louislam/ping": "~0.4.4-mod.1",
|
||||||
"@louislam/sqlite3": "15.1.6",
|
"@louislam/sqlite3": "15.1.6",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.27.0",
|
"axios": "~0.27.0",
|
||||||
@@ -96,10 +100,14 @@
|
|||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
"https-proxy-agent": "~5.0.1",
|
"https-proxy-agent": "~5.0.1",
|
||||||
"iconv-lite": "~0.6.3",
|
"iconv-lite": "~0.6.3",
|
||||||
|
"isomorphic-ws": "^5.0.0",
|
||||||
"jsesc": "~3.0.2",
|
"jsesc": "~3.0.2",
|
||||||
|
"jsonata": "^2.0.3",
|
||||||
"jsonwebtoken": "~9.0.0",
|
"jsonwebtoken": "~9.0.0",
|
||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
|
"kafkajs": "^2.2.4",
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
|
"liquidjs": "^10.7.0",
|
||||||
"mongodb": "~4.14.0",
|
"mongodb": "~4.14.0",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~8.1.4",
|
"mssql": "~8.1.4",
|
||||||
@@ -108,26 +116,31 @@
|
|||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
|
"nostr-tools": "^1.13.1",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
|
"openid-client": "^5.4.2",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"pg": "~8.8.0",
|
"pg": "~8.8.0",
|
||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.5.0",
|
||||||
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"protobufjs": "~7.1.1",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
"redis": "~4.5.1",
|
"redis": "~4.5.1",
|
||||||
|
"semver": "~7.5.4",
|
||||||
"socket.io": "~4.6.1",
|
"socket.io": "~4.6.1",
|
||||||
"socket.io-client": "~4.6.1",
|
"socket.io-client": "~4.6.1",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "~6.1.11",
|
"tar": "~6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2"
|
"thirty-two": "~1.0.2",
|
||||||
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "~5.0.1",
|
"@actions/github": "~5.0.1",
|
||||||
"@babel/eslint-parser": "~7.17.0",
|
"@babel/eslint-parser": "^7.22.7",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
@@ -135,12 +148,11 @@
|
|||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~2.1.0",
|
"@vitejs/plugin-legacy": "~4.1.0",
|
||||||
"@vitejs/plugin-vue": "~3.1.0",
|
"@vitejs/plugin-vue": "~4.2.3",
|
||||||
"@vue/compiler-sfc": "~3.2.36",
|
"@vue/compiler-sfc": "~3.3.4",
|
||||||
"@vuepic/vue-datepicker": "~3.4.8",
|
"@vuepic/vue-datepicker": "~3.4.8",
|
||||||
"aedes": "^0.46.3",
|
"aedes": "^0.46.3",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"chart.js": "~4.2.1",
|
"chart.js": "~4.2.1",
|
||||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
"chartjs-adapter-dayjs-4": "~1.0.4",
|
||||||
@@ -148,16 +160,16 @@
|
|||||||
"core-js": "~3.26.1",
|
"core-js": "~3.26.1",
|
||||||
"cronstrue": "~2.24.0",
|
"cronstrue": "~2.24.0",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"cypress": "^10.1.0",
|
"cypress": "^12.17.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"dompurify": "~2.4.3",
|
"dompurify": "~2.4.3",
|
||||||
"eslint": "~8.14.0",
|
"eslint": "~8.14.0",
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"jest": "~27.2.5",
|
"jest": "~29.6.1",
|
||||||
"marked": "~4.2.5",
|
"marked": "~4.2.5",
|
||||||
"node-ssh": "~13.0.1",
|
"node-ssh": "~13.1.0",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.5.0",
|
||||||
"postcss-rtlcss": "~3.7.2",
|
"postcss-rtlcss": "~3.7.2",
|
||||||
"postcss-scss": "~4.0.4",
|
"postcss-scss": "~4.0.4",
|
||||||
@@ -165,15 +177,16 @@
|
|||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"rollup-plugin-visualizer": "^5.6.0",
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "~15.9.0",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-standard": "~25.0.0",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"terser": "~5.15.0",
|
"terser": "~5.15.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vite": "~3.2.7",
|
"vite": "~4.4.1",
|
||||||
|
"vite-plugin-commonjs": "^0.8.0",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "~3.2.47",
|
"vue": "~3.3.4",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chartjs": "~5.2.0",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
|
@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
|
|||||||
/**
|
/**
|
||||||
* Emits the version information to the client.
|
* Emits the version information to the client.
|
||||||
* @param {Socket} socket Socket.io socket instance
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @param {boolean} hideVersion
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendInfo(socket) {
|
async function sendInfo(socket, hideVersion = false) {
|
||||||
|
let version;
|
||||||
|
let latestVersion;
|
||||||
|
|
||||||
|
if (!hideVersion) {
|
||||||
|
version = checkVersion.version;
|
||||||
|
latestVersion = checkVersion.latestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version: checkVersion.version,
|
version,
|
||||||
latestVersion: checkVersion.latestVersion,
|
latestVersion,
|
||||||
primaryBaseURL: await setting("primaryBaseURL"),
|
primaryBaseURL: await setting("primaryBaseURL"),
|
||||||
serverTimezone: await server.getTimezone(),
|
serverTimezone: await server.getTimezone(),
|
||||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
const args = require("args-parser")(process.argv);
|
// Interop with browser
|
||||||
|
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||||
const demoMode = args["demo"] || false;
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
const badgeConstants = {
|
const badgeConstants = {
|
||||||
|
@@ -3,7 +3,7 @@ const { R } = require("redbean-node");
|
|||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const { log, sleep } = require("../src/util");
|
const { log, sleep } = require("../src/util");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database & App Data Folder
|
* Database & App Data Folder
|
||||||
@@ -22,8 +22,12 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
static uploadDir;
|
static uploadDir;
|
||||||
|
|
||||||
|
static screenshotDir;
|
||||||
|
|
||||||
static path;
|
static path;
|
||||||
|
|
||||||
|
static dockerTLSDir;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
@@ -70,6 +74,13 @@ class Database {
|
|||||||
"patch-monitor-tls.sql": true,
|
"patch-monitor-tls.sql": true,
|
||||||
"patch-maintenance-cron.sql": true,
|
"patch-maintenance-cron.sql": true,
|
||||||
"patch-add-parent-monitor.sql": true,
|
"patch-add-parent-monitor.sql": true,
|
||||||
|
"patch-add-invert-keyword.sql": true,
|
||||||
|
"patch-added-json-query.sql": true,
|
||||||
|
"patch-added-kafka-producer.sql": true,
|
||||||
|
"patch-add-certificate-expiry-status-page.sql": true,
|
||||||
|
"patch-monitor-oauth-cc.sql": true,
|
||||||
|
"patch-add-timeout-monitor.sql": true,
|
||||||
|
"patch-add-gamedig-given-port.sql": true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,17 +99,28 @@ class Database {
|
|||||||
// Data Directory (must be end with "/")
|
// Data Directory (must be end with "/")
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
|
|
||||||
Database.path = Database.dataDir + "kuma.db";
|
Database.path = path.join(Database.dataDir, "kuma.db");
|
||||||
if (! fs.existsSync(Database.dataDir)) {
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.uploadDir = Database.dataDir + "upload/";
|
Database.uploadDir = path.join(Database.dataDir, "upload/");
|
||||||
|
|
||||||
if (! fs.existsSync(Database.uploadDir)) {
|
if (! fs.existsSync(Database.uploadDir)) {
|
||||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create screenshot dir
|
||||||
|
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
|
||||||
|
if (! fs.existsSync(Database.screenshotDir)) {
|
||||||
|
fs.mkdirSync(Database.screenshotDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
|
||||||
|
if (! fs.existsSync(Database.dockerTLSDir)) {
|
||||||
|
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +177,12 @@ class Database {
|
|||||||
await R.exec("PRAGMA journal_mode = WAL");
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
}
|
}
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
||||||
|
|
||||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
// FULL synchronous is very safe, but it is also slower.
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
await R.exec("PRAGMA synchronous = FULL");
|
await R.exec("PRAGMA synchronous = NORMAL");
|
||||||
|
|
||||||
if (!noLog) {
|
if (!noLog) {
|
||||||
log.info("db", "SQLite config:");
|
log.info("db", "SQLite config:");
|
||||||
|
@@ -2,8 +2,16 @@ const axios = require("axios");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const version = require("../package.json").version;
|
const version = require("../package.json").version;
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Database = require("./database");
|
||||||
|
|
||||||
class DockerHost {
|
class DockerHost {
|
||||||
|
|
||||||
|
static CertificateFileNameCA = "ca.pem";
|
||||||
|
static CertificateFileNameCert = "cert.pem";
|
||||||
|
static CertificateFileNameKey = "key.pem";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a docker host
|
* Save a docker host
|
||||||
* @param {Object} dockerHost Docker host to save
|
* @param {Object} dockerHost Docker host to save
|
||||||
@@ -66,10 +74,6 @@ class DockerHost {
|
|||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version
|
"User-Agent": "Uptime-Kuma/" + version
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dockerHost.dockerType === "socket") {
|
if (dockerHost.dockerType === "socket") {
|
||||||
@@ -77,6 +81,7 @@ class DockerHost {
|
|||||||
} else if (dockerHost.dockerType === "tcp") {
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||||
}
|
}
|
||||||
|
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||||
|
|
||||||
let res = await axios.request(options);
|
let res = await axios.request(options);
|
||||||
|
|
||||||
@@ -111,6 +116,53 @@ class DockerHost {
|
|||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTPS agent options with client side TLS parameters if certificate files
|
||||||
|
* for the given host are available under a predefined directory path.
|
||||||
|
*
|
||||||
|
* The base path where certificates are looked for can be set with the
|
||||||
|
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
|
||||||
|
*
|
||||||
|
* If a directory in this path exists with a name matching the FQDN of the docker host
|
||||||
|
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
|
||||||
|
* 'data/docker-tls/example.com/' would be searched for certificate files),
|
||||||
|
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
|
||||||
|
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
|
||||||
|
*
|
||||||
|
* @param {String} dockerType i.e. "tcp" or "socket"
|
||||||
|
* @param {String} url The docker host URL rewritten to https://
|
||||||
|
* @return {Object}
|
||||||
|
* */
|
||||||
|
static getHttpsAgentOptions(dockerType, url) {
|
||||||
|
let baseOptions = {
|
||||||
|
maxCachedSessions: 0,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
};
|
||||||
|
let certOptions = {};
|
||||||
|
|
||||||
|
let dirName = (new URL(url)).hostname;
|
||||||
|
|
||||||
|
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
|
||||||
|
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
||||||
|
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
||||||
|
|
||||||
|
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
||||||
|
let ca = fs.readFileSync(caPath);
|
||||||
|
let key = fs.readFileSync(keyPath);
|
||||||
|
let cert = fs.readFileSync(certPath);
|
||||||
|
certOptions = {
|
||||||
|
ca,
|
||||||
|
key,
|
||||||
|
cert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseOptions,
|
||||||
|
...certOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
const { clearOldData } = require("./jobs/clear-old-data");
|
const { clearOldData } = require("./jobs/clear-old-data");
|
||||||
|
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
|
||||||
const Cron = require("croner");
|
const Cron = require("croner");
|
||||||
|
|
||||||
const jobs = [
|
const jobs = [
|
||||||
@@ -9,6 +10,12 @@ const jobs = [
|
|||||||
jobFunc: clearOldData,
|
jobFunc: clearOldData,
|
||||||
croner: null,
|
croner: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "incremental-vacuum",
|
||||||
|
interval: "*/5 * * * *",
|
||||||
|
jobFunc: incrementalVacuum,
|
||||||
|
croner: null,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -39,6 +39,8 @@ const clearOldData = async () => {
|
|||||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||||
[ parsedPeriod ]
|
[ parsedPeriod ]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await R.exec("PRAGMA optimize;");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
21
server/jobs/incremental-vacuum.js
Normal file
21
server/jobs/incremental-vacuum.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run incremental_vacuum and checkpoint the WAL.
|
||||||
|
* @return {Promise<void>} A promise that resolves when the process is finished.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const incrementalVacuum = async () => {
|
||||||
|
try {
|
||||||
|
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
|
||||||
|
await R.exec("PRAGMA incremental_vacuum(200)");
|
||||||
|
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
||||||
|
} catch (e) {
|
||||||
|
log.error("incrementalVacuum", `Failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
incrementalVacuum,
|
||||||
|
};
|
@@ -9,12 +9,12 @@ class Group extends BeanModel {
|
|||||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON(showTags = false) {
|
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON(showTags));
|
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
|
|||||||
SQL_DATETIME_FORMAT
|
SQL_DATETIME_FORMAT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, mongodbPing,
|
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
@@ -20,6 +20,8 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
|||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
|
const jsonata = require("jsonata");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -35,11 +37,12 @@ class Monitor extends BeanModel {
|
|||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON(showTags = false) {
|
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||||
let obj = {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
sendUrl: this.sendUrl,
|
sendUrl: this.sendUrl,
|
||||||
|
type: this.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.sendUrl) {
|
if (this.sendUrl) {
|
||||||
@@ -49,6 +52,13 @@ class Monitor extends BeanModel {
|
|||||||
if (showTags) {
|
if (showTags) {
|
||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certExpiry && this.type === "http") {
|
||||||
|
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
||||||
|
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
||||||
|
obj.validCert = validCert;
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +80,12 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
const tags = await this.getTags();
|
const tags = await this.getTags();
|
||||||
|
|
||||||
|
let screenshot = null;
|
||||||
|
|
||||||
|
if (this.type === "real-browser") {
|
||||||
|
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||||
|
}
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -86,10 +102,12 @@ class Monitor extends BeanModel {
|
|||||||
active: await this.isActive(),
|
active: await this.isActive(),
|
||||||
forceInactive: !await Monitor.isParentActive(this.id),
|
forceInactive: !await Monitor.isParentActive(this.id),
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
timeout: this.timeout,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
resendInterval: this.resendInterval,
|
resendInterval: this.resendInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
|
invertKeyword: this.isInvertKeyword(),
|
||||||
expiryNotification: this.isEnabledExpiryNotification(),
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
@@ -117,7 +135,16 @@ class Monitor extends BeanModel {
|
|||||||
radiusCalledStationId: this.radiusCalledStationId,
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
radiusCallingStationId: this.radiusCallingStationId,
|
radiusCallingStationId: this.radiusCallingStationId,
|
||||||
game: this.game,
|
game: this.game,
|
||||||
httpBodyEncoding: this.httpBodyEncoding
|
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
|
||||||
|
httpBodyEncoding: this.httpBodyEncoding,
|
||||||
|
jsonPath: this.jsonPath,
|
||||||
|
expectedValue: this.expectedValue,
|
||||||
|
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||||
|
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||||
|
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
||||||
|
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
||||||
|
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||||
|
screenshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@@ -129,6 +156,11 @@ class Monitor extends BeanModel {
|
|||||||
grpcMetadata: this.grpcMetadata,
|
grpcMetadata: this.grpcMetadata,
|
||||||
basic_auth_user: this.basic_auth_user,
|
basic_auth_user: this.basic_auth_user,
|
||||||
basic_auth_pass: this.basic_auth_pass,
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
|
oauth_client_id: this.oauth_client_id,
|
||||||
|
oauth_client_secret: this.oauth_client_secret,
|
||||||
|
oauth_token_url: this.oauth_token_url,
|
||||||
|
oauth_scopes: this.oauth_scopes,
|
||||||
|
oauth_auth_method: this.oauth_auth_method,
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
databaseConnectionString: this.databaseConnectionString,
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
radiusUsername: this.radiusUsername,
|
radiusUsername: this.radiusUsername,
|
||||||
@@ -141,6 +173,7 @@ class Monitor extends BeanModel {
|
|||||||
tlsCa: this.tlsCa,
|
tlsCa: this.tlsCa,
|
||||||
tlsCert: this.tlsCert,
|
tlsCert: this.tlsCert,
|
||||||
tlsKey: this.tlsKey,
|
tlsKey: this.tlsKey,
|
||||||
|
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +188,7 @@ class Monitor extends BeanModel {
|
|||||||
async isActive() {
|
async isActive() {
|
||||||
const parentActive = await Monitor.isParentActive(this.id);
|
const parentActive = await Monitor.isParentActive(this.id);
|
||||||
|
|
||||||
return this.active && parentActive;
|
return (this.active === 1) && parentActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,6 +199,31 @@ class Monitor extends BeanModel {
|
|||||||
return 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 = ? ORDER BY tag.name", [ this.id ]);
|
return 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 = ? ORDER BY tag.name", [ this.id ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets certificate expiry for this monitor
|
||||||
|
* @param {number} monitorID ID of monitor to send
|
||||||
|
* @returns {Promise<LooseObject<any>>}
|
||||||
|
*/
|
||||||
|
async getCertExpiry(monitorID) {
|
||||||
|
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
let tlsInfo;
|
||||||
|
if (tlsInfoBean) {
|
||||||
|
tlsInfo = JSON.parse(tlsInfoBean?.info_json);
|
||||||
|
if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) {
|
||||||
|
return {
|
||||||
|
certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining,
|
||||||
|
validCert: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
certExpiryDaysRemaining: "",
|
||||||
|
validCert: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode user and password to Base64 encoding
|
* Encode user and password to Base64 encoding
|
||||||
* for HTTP "basic" auth, as per RFC-7617
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
@@ -199,6 +257,14 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isInvertKeyword() {
|
||||||
|
return Boolean(this.invertKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse to boolean
|
* Parse to boolean
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -215,6 +281,10 @@ class Monitor extends BeanModel {
|
|||||||
return JSON.parse(this.accepted_statuscodes_json);
|
return JSON.parse(this.accepted_statuscodes_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGameDigGivenPortOnly() {
|
||||||
|
return Boolean(this.gamedigGivenPortOnly);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@@ -287,7 +357,10 @@ class Monitor extends BeanModel {
|
|||||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||||
|
|
||||||
// Only change state if the monitor is in worse conditions then the ones before
|
// Only change state if the monitor is in worse conditions then the ones before
|
||||||
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
// lastBeat.status could be null
|
||||||
|
if (!lastBeat) {
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||||
bean.status = lastBeat.status;
|
bean.status = lastBeat.status;
|
||||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||||
bean.status = lastBeat.status;
|
bean.status = lastBeat.status;
|
||||||
@@ -303,7 +376,7 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = "Group empty";
|
bean.msg = "Group empty";
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "http" || this.type === "keyword") {
|
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
@@ -315,6 +388,24 @@ class Monitor extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDC: Basic client credential flow.
|
||||||
|
// Additional grants might be implemented in the future
|
||||||
|
let oauth2AuthHeader = {};
|
||||||
|
if (this.auth_method === "oauth2-cc") {
|
||||||
|
try {
|
||||||
|
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
||||||
|
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
|
||||||
|
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
||||||
|
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
|
||||||
|
}
|
||||||
|
oauth2AuthHeader = {
|
||||||
|
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("The oauth config is invalid. " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const httpsAgentOptions = {
|
const httpsAgentOptions = {
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
@@ -343,12 +434,13 @@ class Monitor extends BeanModel {
|
|||||||
const options = {
|
const options = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: (this.method || "get").toLowerCase(),
|
method: (this.method || "get").toLowerCase(),
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
...(contentType ? { "Content-Type": contentType } : {}),
|
...(contentType ? { "Content-Type": contentType } : {}),
|
||||||
...(basicAuthHeader),
|
...(basicAuthHeader),
|
||||||
|
...(oauth2AuthHeader),
|
||||||
...(this.headers ? JSON.parse(this.headers) : {})
|
...(this.headers ? JSON.parse(this.headers) : {})
|
||||||
},
|
},
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
@@ -431,7 +523,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else if (this.type === "keyword") {
|
||||||
|
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|
||||||
@@ -440,17 +532,37 @@ class Monitor extends BeanModel {
|
|||||||
data = JSON.stringify(data);
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
let keywordFound = data.includes(this.keyword);
|
||||||
bean.msg += ", keyword is found";
|
if (keywordFound === !this.isInvertKeyword()) {
|
||||||
|
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||||
if (data.length > 50) {
|
if (data.length > 50) {
|
||||||
data = data.substring(0, 47) + "...";
|
data = data.substring(0, 47) + "...";
|
||||||
}
|
}
|
||||||
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
throw new Error(bean.msg + ", but keyword is " +
|
||||||
|
(keywordFound ? "present" : "not") + " in [" + data + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (this.type === "json-query") {
|
||||||
|
let data = res.data;
|
||||||
|
|
||||||
|
// convert data to object
|
||||||
|
if (typeof data === "string") {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let expression = jsonata(this.jsonPath);
|
||||||
|
|
||||||
|
let result = await expression.evaluate(data);
|
||||||
|
|
||||||
|
if (result.toString() === this.expectedValue) {
|
||||||
|
bean.msg += ", expected value is found";
|
||||||
|
bean.status = UP;
|
||||||
|
} else {
|
||||||
|
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
@@ -525,7 +637,7 @@ class Monitor extends BeanModel {
|
|||||||
// No need to insert successful heartbeat for push type, so end here
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
retries = 0;
|
retries = 0;
|
||||||
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
this.heartbeatInterval = setTimeout(beat, timeout);
|
this.heartbeatInterval = setTimeout(safeBeat, timeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -542,7 +654,7 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let res = await axios.get(steamApiUrl, {
|
let res = await axios.get(steamApiUrl, {
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
@@ -580,7 +692,7 @@ class Monitor extends BeanModel {
|
|||||||
type: this.game,
|
type: this.game,
|
||||||
host: this.hostname,
|
host: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
givenPortOnly: true,
|
givenPortOnly: this.getGameDigGivenPortOnly(),
|
||||||
});
|
});
|
||||||
|
|
||||||
bean.msg = state.name;
|
bean.msg = state.name;
|
||||||
@@ -614,13 +726,22 @@ class Monitor extends BeanModel {
|
|||||||
options.socketPath = dockerHost._dockerDaemon;
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
} else if (dockerHost._dockerType === "tcp") {
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||||
|
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
|
||||||
|
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
let res = await axios.request(options);
|
let res = await axios.request(options);
|
||||||
|
|
||||||
if (res.data.State.Running) {
|
if (res.data.State.Running) {
|
||||||
bean.status = UP;
|
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
|
||||||
bean.msg = res.data.State.Status;
|
bean.status = PENDING;
|
||||||
|
bean.msg = res.data.State.Health.Status;
|
||||||
|
} else {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Error("Container State is " + res.data.State.Status);
|
throw Error("Container State is " + res.data.State.Status);
|
||||||
}
|
}
|
||||||
@@ -649,7 +770,6 @@ class Monitor extends BeanModel {
|
|||||||
grpcEnableTls: this.grpcEnableTls,
|
grpcEnableTls: this.grpcEnableTls,
|
||||||
grpcMethod: this.grpcMethod,
|
grpcMethod: this.grpcMethod,
|
||||||
grpcBody: this.grpcBody,
|
grpcBody: this.grpcBody,
|
||||||
keyword: this.keyword
|
|
||||||
};
|
};
|
||||||
const response = await grpcQuery(options);
|
const response = await grpcQuery(options);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
@@ -662,13 +782,14 @@ class Monitor extends BeanModel {
|
|||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||||
} else {
|
} else {
|
||||||
if (response.data.toString().includes(this.keyword)) {
|
let keywordFound = response.data.toString().includes(this.keyword);
|
||||||
|
if (keywordFound === !this.isInvertKeyword()) {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||||
} else {
|
} else {
|
||||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
@@ -707,28 +828,19 @@ class Monitor extends BeanModel {
|
|||||||
port = this.port;
|
port = this.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const resp = await radius(
|
||||||
const resp = await radius(
|
this.hostname,
|
||||||
this.hostname,
|
this.radiusUsername,
|
||||||
this.radiusUsername,
|
this.radiusPassword,
|
||||||
this.radiusPassword,
|
this.radiusCalledStationId,
|
||||||
this.radiusCalledStationId,
|
this.radiusCallingStationId,
|
||||||
this.radiusCallingStationId,
|
this.radiusSecret,
|
||||||
this.radiusSecret,
|
port,
|
||||||
port
|
this.interval * 1000 * 0.4,
|
||||||
);
|
);
|
||||||
if (resp.code) {
|
|
||||||
bean.msg = resp.code;
|
bean.msg = resp.code;
|
||||||
}
|
bean.status = UP;
|
||||||
bean.status = UP;
|
|
||||||
} catch (error) {
|
|
||||||
bean.status = DOWN;
|
|
||||||
if (error.response?.code) {
|
|
||||||
bean.msg = error.response.code;
|
|
||||||
} else {
|
|
||||||
bean.msg = error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else if (this.type === "redis") {
|
} else if (this.type === "redis") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
@@ -740,11 +852,29 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||||
await monitorType.check(this, bean);
|
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
|
||||||
if (!bean.ping) {
|
if (!bean.ping) {
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (this.type === "kafka-producer") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
bean.msg = await kafkaProducerAsync(
|
||||||
|
JSON.parse(this.kafkaProducerBrokers),
|
||||||
|
this.kafkaProducerTopic,
|
||||||
|
this.kafkaProducerMessage,
|
||||||
|
{
|
||||||
|
allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
|
||||||
|
ssl: this.kafkaProducerSsl,
|
||||||
|
clientId: `Uptime-Kuma/${version}`,
|
||||||
|
interval: this.interval,
|
||||||
|
},
|
||||||
|
JSON.parse(this.kafkaProducerSaslOptions),
|
||||||
|
);
|
||||||
|
bean.status = UP;
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown Monitor Type");
|
throw new Error("Unknown Monitor Type");
|
||||||
}
|
}
|
||||||
|
@@ -90,6 +90,8 @@ class StatusPage extends BeanModel {
|
|||||||
* @param {StatusPage} statusPage
|
* @param {StatusPage} statusPage
|
||||||
*/
|
*/
|
||||||
static async getStatusPageData(statusPage) {
|
static async getStatusPageData(statusPage) {
|
||||||
|
const config = await statusPage.toPublicJSON();
|
||||||
|
|
||||||
// Incident
|
// Incident
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
statusPage.id,
|
statusPage.id,
|
||||||
@@ -110,13 +112,13 @@ class StatusPage extends BeanModel {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for (let groupBean of list) {
|
for (let groupBean of list) {
|
||||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
|
||||||
publicGroupList.push(monitorGroup);
|
publicGroupList.push(monitorGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response
|
// Response
|
||||||
return {
|
return {
|
||||||
config: await statusPage.toPublicJSON(),
|
config,
|
||||||
incident,
|
incident,
|
||||||
publicGroupList,
|
publicGroupList,
|
||||||
maintenanceList,
|
maintenanceList,
|
||||||
@@ -234,6 +236,7 @@ class StatusPage extends BeanModel {
|
|||||||
footerText: this.footer_text,
|
footerText: this.footer_text,
|
||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +258,7 @@ class StatusPage extends BeanModel {
|
|||||||
footerText: this.footer_text,
|
footerText: this.footer_text,
|
||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,9 +6,10 @@ class MonitorType {
|
|||||||
*
|
*
|
||||||
* @param {Monitor} monitor
|
* @param {Monitor} monitor
|
||||||
* @param {Heartbeat} heartbeat
|
* @param {Heartbeat} heartbeat
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async check(monitor, heartbeat) {
|
async check(monitor, heartbeat, server) {
|
||||||
throw new Error("You need to override check()");
|
throw new Error("You need to override check()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
212
server/monitor-types/real-browser-monitor-type.js
Normal file
212
server/monitor-types/real-browser-monitor-type.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { chromium } = require("playwright-core");
|
||||||
|
const { UP, log } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
const commandExistsSync = require("command-exists").sync;
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
const Database = require("../database");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
|
let browser = null;
|
||||||
|
|
||||||
|
let allowedList = [];
|
||||||
|
let lastAutoDetectChromeExecutable = null;
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||||
|
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||||
|
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||||
|
|
||||||
|
// Allow Chromium too
|
||||||
|
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
|
||||||
|
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||||
|
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||||
|
|
||||||
|
// For Loop A to Z
|
||||||
|
for (let i = 65; i <= 90; i++) {
|
||||||
|
let drive = String.fromCharCode(i);
|
||||||
|
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
||||||
|
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (process.platform === "linux") {
|
||||||
|
allowedList = [
|
||||||
|
"chromium",
|
||||||
|
"chromium-browser",
|
||||||
|
"google-chrome",
|
||||||
|
|
||||||
|
"/usr/bin/chromium",
|
||||||
|
"/usr/bin/chromium-browser",
|
||||||
|
"/usr/bin/google-chrome",
|
||||||
|
];
|
||||||
|
} else if (process.platform === "darwin") {
|
||||||
|
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||||
|
allowedList = [
|
||||||
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("chrome", allowedList);
|
||||||
|
|
||||||
|
async function isAllowedChromeExecutable(executablePath) {
|
||||||
|
console.log(config.args);
|
||||||
|
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the executablePath is in the list of allowed executables
|
||||||
|
return allowedList.includes(executablePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBrowser() {
|
||||||
|
if (!browser) {
|
||||||
|
let executablePath = await Settings.get("chromeExecutable");
|
||||||
|
|
||||||
|
executablePath = await prepareChromeExecutable(executablePath);
|
||||||
|
|
||||||
|
browser = await chromium.launch({
|
||||||
|
//headless: false,
|
||||||
|
executablePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareChromeExecutable(executablePath) {
|
||||||
|
// Special code for using the playwright_chromium
|
||||||
|
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
||||||
|
// Set to undefined = use playwright_chromium
|
||||||
|
executablePath = undefined;
|
||||||
|
} else if (!executablePath) {
|
||||||
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
|
executablePath = "/usr/bin/chromium";
|
||||||
|
|
||||||
|
// Install chromium in container via apt install
|
||||||
|
if ( !commandExistsSync(executablePath)) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
log.info("Chromium", "Installing Chromium...");
|
||||||
|
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
||||||
|
|
||||||
|
// On exit
|
||||||
|
child.on("exit", (code) => {
|
||||||
|
log.info("Chromium", "apt install chromium exited with code " + code);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
log.info("Chromium", "Installed Chromium");
|
||||||
|
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
|
||||||
|
log.info("Chromium", "Chromium version: " + version);
|
||||||
|
resolve();
|
||||||
|
} else if (code === 100) {
|
||||||
|
reject(new Error("Installing Chromium, please wait..."));
|
||||||
|
} else {
|
||||||
|
reject(new Error("apt install chromium failed with code " + code));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
executablePath = findChrome(allowedList);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User specified a path
|
||||||
|
// Check if the executablePath is in the list of allowed
|
||||||
|
if (!await isAllowedChromeExecutable(executablePath)) {
|
||||||
|
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return executablePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChrome(executables) {
|
||||||
|
// Use the last working executable, so we don't have to search for it again
|
||||||
|
if (lastAutoDetectChromeExecutable) {
|
||||||
|
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
||||||
|
return lastAutoDetectChromeExecutable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let executable of executables) {
|
||||||
|
if (commandExistsSync(executable)) {
|
||||||
|
lastAutoDetectChromeExecutable = executable;
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetChrome() {
|
||||||
|
if (browser) {
|
||||||
|
await browser.close();
|
||||||
|
browser = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the chrome executable is valid and return the version
|
||||||
|
* @param executablePath
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function testChrome(executablePath) {
|
||||||
|
try {
|
||||||
|
executablePath = await prepareChromeExecutable(executablePath);
|
||||||
|
|
||||||
|
log.info("Chromium", "Testing Chromium executable: " + executablePath);
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
executablePath,
|
||||||
|
});
|
||||||
|
const version = browser.version();
|
||||||
|
await browser.close();
|
||||||
|
return version;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class RealBrowserMonitorType extends MonitorType {
|
||||||
|
|
||||||
|
name = "real-browser";
|
||||||
|
|
||||||
|
async check(monitor, heartbeat, server) {
|
||||||
|
const browser = await getBrowser();
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
const res = await page.goto(monitor.url, {
|
||||||
|
waitUntil: "networkidle",
|
||||||
|
timeout: monitor.interval * 1000 * 0.8,
|
||||||
|
});
|
||||||
|
|
||||||
|
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(Database.screenshotDir, filename),
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
|
||||||
|
if (res.status() >= 200 && res.status() < 400) {
|
||||||
|
heartbeat.status = UP;
|
||||||
|
heartbeat.msg = res.status();
|
||||||
|
|
||||||
|
const timing = res.request().timing();
|
||||||
|
heartbeat.ping = timing.responseEnd;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.status() + "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RealBrowserMonitorType,
|
||||||
|
testChrome,
|
||||||
|
resetChrome,
|
||||||
|
};
|
95
server/monitor-types/tailscale-ping.js
Normal file
95
server/monitor-types/tailscale-ping.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { UP, log } = require("../../src/util");
|
||||||
|
const exec = require("child_process").exec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TailscalePing class extends the MonitorType.
|
||||||
|
* It runs Tailscale ping to monitor the status of a specific node.
|
||||||
|
*/
|
||||||
|
class TailscalePing extends MonitorType {
|
||||||
|
|
||||||
|
name = "tailscale-ping";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the ping status of the URL associated with the monitor.
|
||||||
|
* It then parses the Tailscale ping command output to update the heatrbeat.
|
||||||
|
*
|
||||||
|
* @param {Object} monitor - The monitor object associated with the check.
|
||||||
|
* @param {Object} heartbeat - The heartbeat object to update.
|
||||||
|
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat) {
|
||||||
|
try {
|
||||||
|
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||||
|
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||||
|
} catch (err) {
|
||||||
|
log.debug("Tailscale", err);
|
||||||
|
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||||
|
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the Tailscale ping command to the given URL.
|
||||||
|
*
|
||||||
|
* @param {string} hostname - The hostname to ping.
|
||||||
|
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||||
|
* @throws Will throw an error if the command execution encounters any error.
|
||||||
|
*/
|
||||||
|
async runTailscalePing(hostname, interval) {
|
||||||
|
let cmd = `tailscale ping ${hostname}`;
|
||||||
|
|
||||||
|
log.debug("Tailscale", cmd);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timeout = interval * 1000 * 0.8;
|
||||||
|
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
||||||
|
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
||||||
|
if (error) {
|
||||||
|
reject(`Execution error: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
reject(`Error in output: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(stdout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the output of the Tailscale ping command to update the heartbeat.
|
||||||
|
*
|
||||||
|
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
|
||||||
|
* @param {Object} heartbeat - The heartbeat object to update.
|
||||||
|
* @throws Will throw an eror if the output contains any unexpected string.
|
||||||
|
*/
|
||||||
|
parseTailscaleOutput(tailscaleOutput, heartbeat) {
|
||||||
|
let lines = tailscaleOutput.split("\n");
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
if (line.includes("pong from")) {
|
||||||
|
heartbeat.status = UP;
|
||||||
|
let time = line.split(" in ")[1].split(" ")[0];
|
||||||
|
heartbeat.ping = parseInt(time);
|
||||||
|
heartbeat.msg = line;
|
||||||
|
break;
|
||||||
|
} else if (line.includes("timed out")) {
|
||||||
|
throw new Error(`Ping timed out: "${line}"`);
|
||||||
|
// Immediately throws upon "timed out" message, the server is expected to re-call the check function
|
||||||
|
} else if (line.includes("no matching peer")) {
|
||||||
|
throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`);
|
||||||
|
} else if (line.includes("is local Tailscale IP")) {
|
||||||
|
throw new Error(`Tailscale only works if used on other machines: "${line}"`);
|
||||||
|
} else if (line !== "") {
|
||||||
|
throw new Error(`Unexpected output: "${line}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TailscalePing,
|
||||||
|
};
|
98
server/notification-providers/flashduty.js
Normal file
98
server/notification-providers/flashduty.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
const successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
|
class FlashDuty extends NotificationProvider {
|
||||||
|
name = "FlashDuty";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const title = "Uptime Kuma Alert";
|
||||||
|
const monitor = {
|
||||||
|
type: "ping",
|
||||||
|
url: msg,
|
||||||
|
name: "https://flashcat.cloud"
|
||||||
|
};
|
||||||
|
return this.postNotification(notification, title, msg, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === UP) {
|
||||||
|
const title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === DOWN) {
|
||||||
|
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generate a monitor url from the monitors infomation
|
||||||
|
* @param {Object} monitorInfo Monitor details
|
||||||
|
* @returns {string|undefined}
|
||||||
|
*/
|
||||||
|
|
||||||
|
genMonitorUrl(monitorInfo) {
|
||||||
|
if (monitorInfo.type === "port" && monitorInfo.port) {
|
||||||
|
return monitorInfo.hostname + ":" + monitorInfo.port;
|
||||||
|
}
|
||||||
|
if (monitorInfo.hostname != null) {
|
||||||
|
return monitorInfo.hostname;
|
||||||
|
}
|
||||||
|
return monitorInfo.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {BeanModel} notification Message title
|
||||||
|
* @param {string} title Message
|
||||||
|
* @param {string} body Message
|
||||||
|
* @param {Object} monitorInfo Monitor details
|
||||||
|
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: {
|
||||||
|
description: `[${title}] [${monitorInfo.name}] ${body}`,
|
||||||
|
title,
|
||||||
|
event_status: eventStatus || "Info",
|
||||||
|
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
||||||
|
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
||||||
|
[item.name]: item.value
|
||||||
|
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorInfo) {
|
||||||
|
options.client = "Uptime Kuma";
|
||||||
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await axios.request(options);
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("FlashDuty notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("FlashDuty notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "FlashDuty notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FlashDuty;
|
@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||||
{
|
{
|
||||||
title: "Uptime Kuma",
|
title: "Uptime Kuma",
|
||||||
message,
|
message,
|
||||||
|
119
server/notification-providers/nostr.js
Normal file
119
server/notification-providers/nostr.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
const { log } = require("../../src/util");
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const {
|
||||||
|
relayInit,
|
||||||
|
getPublicKey,
|
||||||
|
getEventHash,
|
||||||
|
getSignature,
|
||||||
|
nip04,
|
||||||
|
nip19
|
||||||
|
} = require("nostr-tools");
|
||||||
|
|
||||||
|
// polyfills for node versions
|
||||||
|
const semver = require("semver");
|
||||||
|
const nodeVersion = process.version;
|
||||||
|
if (semver.lt(nodeVersion, "16.0.0")) {
|
||||||
|
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
||||||
|
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
||||||
|
// polyfills for node 16
|
||||||
|
global.crypto = require("crypto");
|
||||||
|
global.WebSocket = require("isomorphic-ws");
|
||||||
|
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
||||||
|
crypto.subtle = crypto.webcrypto.subtle;
|
||||||
|
}
|
||||||
|
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
||||||
|
// polyfills for node 18
|
||||||
|
global.crypto = require("crypto");
|
||||||
|
global.WebSocket = require("isomorphic-ws");
|
||||||
|
} else {
|
||||||
|
// polyfills for node 20
|
||||||
|
global.WebSocket = require("isomorphic-ws");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Nostr extends NotificationProvider {
|
||||||
|
name = "nostr";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
// All DMs should have same timestamp
|
||||||
|
const createdAt = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const senderPrivateKey = await this.getPrivateKey(notification.sender);
|
||||||
|
const senderPublicKey = getPublicKey(senderPrivateKey);
|
||||||
|
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
|
||||||
|
|
||||||
|
// Create NIP-04 encrypted direct message event for each recipient
|
||||||
|
const events = [];
|
||||||
|
for (const recipientPublicKey of recipientsPublicKeys) {
|
||||||
|
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
|
||||||
|
let event = {
|
||||||
|
kind: 4,
|
||||||
|
pubkey: senderPublicKey,
|
||||||
|
created_at: createdAt,
|
||||||
|
tags: [[ "p", recipientPublicKey ]],
|
||||||
|
content: ciphertext,
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = getSignature(event, senderPrivateKey);
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish events to each relay
|
||||||
|
const relays = notification.relays.split("\n");
|
||||||
|
let successfulRelays = 0;
|
||||||
|
|
||||||
|
// Connect to each relay
|
||||||
|
for (const relayUrl of relays) {
|
||||||
|
const relay = relayInit(relayUrl);
|
||||||
|
try {
|
||||||
|
await relay.connect();
|
||||||
|
successfulRelays++;
|
||||||
|
|
||||||
|
// Publish events
|
||||||
|
for (const event of events) {
|
||||||
|
relay.publish(event);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
continue;
|
||||||
|
} finally {
|
||||||
|
relay.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report success or failure
|
||||||
|
if (successfulRelays === 0) {
|
||||||
|
throw Error("Failed to connect to any relays.");
|
||||||
|
}
|
||||||
|
return `${successfulRelays}/${relays.length} relays connected.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrivateKey(sender) {
|
||||||
|
try {
|
||||||
|
const senderDecodeResult = await nip19.decode(sender);
|
||||||
|
const { data } = senderDecodeResult;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get private key: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKeys(recipients) {
|
||||||
|
const recipientsList = recipients.split("\n");
|
||||||
|
const publicKeys = [];
|
||||||
|
for (const recipient of recipientsList) {
|
||||||
|
try {
|
||||||
|
const recipientDecodeResult = await nip19.decode(recipient);
|
||||||
|
const { type, data } = recipientDecodeResult;
|
||||||
|
if (type === "npub") {
|
||||||
|
publicKeys.push(data);
|
||||||
|
} else {
|
||||||
|
throw new Error("not an npub");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error decoding recipient: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return publicKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Nostr;
|
@@ -8,7 +8,9 @@ class PushDeer extends NotificationProvider {
|
|||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let pushdeerlink = "https://api2.pushdeer.com/message/push";
|
let endpoint = "/message/push";
|
||||||
|
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||||
|
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
|
||||||
|
|
||||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||||
|
|
||||||
|
@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
|
|||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
if (notification.slackchannelnotify) {
|
||||||
|
msg += " <!channel>";
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {
|
let data = {
|
||||||
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
|
|||||||
"type": "header",
|
"type": "header",
|
||||||
"text": {
|
"text": {
|
||||||
"type": "plain_text",
|
"type": "plain_text",
|
||||||
"text": "Uptime Kuma Alert",
|
"text": textMsg,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
42
server/notification-providers/smsc.js
Normal file
42
server/notification-providers/smsc.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SMSC extends NotificationProvider {
|
||||||
|
name = "smsc";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "text/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let getArray = [
|
||||||
|
"fmt=3",
|
||||||
|
"translit=" + notification.smscTranslit,
|
||||||
|
"login=" + notification.smscLogin,
|
||||||
|
"psw=" + notification.smscPassword,
|
||||||
|
"phones=" + notification.smscToNumber,
|
||||||
|
"mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
|
||||||
|
];
|
||||||
|
if (notification.smscSenderName !== "") {
|
||||||
|
getArray.push("sender=" + notification.smscSenderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
|
||||||
|
if (resp.data.id === undefined) {
|
||||||
|
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SMSC;
|
@@ -13,7 +13,7 @@ class SMTP extends NotificationProvider {
|
|||||||
port: notification.smtpPort,
|
port: notification.smtpPort,
|
||||||
secure: notification.smtpSecure,
|
secure: notification.smtpSecure,
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
|
|||||||
if (monitorJSON !== null) {
|
if (monitorJSON !== null) {
|
||||||
monitorName = monitorJSON["name"];
|
monitorName = monitorJSON["name"];
|
||||||
|
|
||||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||||
monitorHostnameOrURL = monitorJSON["url"];
|
monitorHostnameOrURL = monitorJSON["url"];
|
||||||
} else {
|
} else {
|
||||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||||
|
@@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
|
|||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
let accountSID = notification.twilioAccountSID;
|
let accountSID = notification.twilioAccountSID;
|
||||||
|
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
|
||||||
let authToken = notification.twilioAuthToken;
|
let authToken = notification.twilioAuthToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
|
|||||||
let config = {
|
let config = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||||
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
|
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const FormData = require("form-data");
|
const FormData = require("form-data");
|
||||||
|
const { Liquid } = require("liquidjs");
|
||||||
|
|
||||||
class Webhook extends NotificationProvider {
|
class Webhook extends NotificationProvider {
|
||||||
|
|
||||||
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
|
|||||||
monitor: monitorJSON,
|
monitor: monitorJSON,
|
||||||
msg,
|
msg,
|
||||||
};
|
};
|
||||||
let finalData;
|
|
||||||
let config = {
|
let config = {
|
||||||
headers: {}
|
headers: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.webhookContentType === "form-data") {
|
if (notification.webhookContentType === "form-data") {
|
||||||
finalData = new FormData();
|
const formData = new FormData();
|
||||||
finalData.append("data", JSON.stringify(data));
|
formData.append("data", JSON.stringify(data));
|
||||||
config.headers = finalData.getHeaders();
|
config.headers = formData.getHeaders();
|
||||||
} else {
|
data = formData;
|
||||||
finalData = data;
|
} else if (notification.webhookContentType === "custom") {
|
||||||
|
// Initialize LiquidJS and parse the custom Body Template
|
||||||
|
const engine = new Liquid();
|
||||||
|
const tpl = engine.parse(notification.webhookCustomBody);
|
||||||
|
|
||||||
|
// Insert templated values into Body
|
||||||
|
data = await engine.render(tpl,
|
||||||
|
{
|
||||||
|
msg,
|
||||||
|
heartbeatJSON,
|
||||||
|
monitorJSON
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.webhookAdditionalHeaders) {
|
if (notification.webhookAdditionalHeaders) {
|
||||||
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post(notification.webhookURL, finalData, config);
|
await axios.post(notification.webhookURL, data, config);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
|
|||||||
const Apprise = require("./notification-providers/apprise");
|
const Apprise = require("./notification-providers/apprise");
|
||||||
const Bark = require("./notification-providers/bark");
|
const Bark = require("./notification-providers/bark");
|
||||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||||
|
const SMSC = require("./notification-providers/smsc");
|
||||||
const DingDing = require("./notification-providers/dingding");
|
const DingDing = require("./notification-providers/dingding");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Discord = require("./notification-providers/discord");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
const Feishu = require("./notification-providers/feishu");
|
||||||
@@ -20,11 +21,13 @@ const LineNotify = require("./notification-providers/linenotify");
|
|||||||
const LunaSea = require("./notification-providers/lunasea");
|
const LunaSea = require("./notification-providers/lunasea");
|
||||||
const Matrix = require("./notification-providers/matrix");
|
const Matrix = require("./notification-providers/matrix");
|
||||||
const Mattermost = require("./notification-providers/mattermost");
|
const Mattermost = require("./notification-providers/mattermost");
|
||||||
|
const Nostr = require("./notification-providers/nostr");
|
||||||
const Ntfy = require("./notification-providers/ntfy");
|
const Ntfy = require("./notification-providers/ntfy");
|
||||||
const Octopush = require("./notification-providers/octopush");
|
const Octopush = require("./notification-providers/octopush");
|
||||||
const OneBot = require("./notification-providers/onebot");
|
const OneBot = require("./notification-providers/onebot");
|
||||||
const Opsgenie = require("./notification-providers/opsgenie");
|
const Opsgenie = require("./notification-providers/opsgenie");
|
||||||
const PagerDuty = require("./notification-providers/pagerduty");
|
const PagerDuty = require("./notification-providers/pagerduty");
|
||||||
|
const FlashDuty = require("./notification-providers/flashduty");
|
||||||
const PagerTree = require("./notification-providers/pagertree");
|
const PagerTree = require("./notification-providers/pagertree");
|
||||||
const PromoSMS = require("./notification-providers/promosms");
|
const PromoSMS = require("./notification-providers/promosms");
|
||||||
const Pushbullet = require("./notification-providers/pushbullet");
|
const Pushbullet = require("./notification-providers/pushbullet");
|
||||||
@@ -68,6 +71,7 @@ class Notification {
|
|||||||
new Apprise(),
|
new Apprise(),
|
||||||
new Bark(),
|
new Bark(),
|
||||||
new ClickSendSMS(),
|
new ClickSendSMS(),
|
||||||
|
new SMSC(),
|
||||||
new DingDing(),
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Feishu(),
|
new Feishu(),
|
||||||
@@ -82,11 +86,13 @@ class Notification {
|
|||||||
new LunaSea(),
|
new LunaSea(),
|
||||||
new Matrix(),
|
new Matrix(),
|
||||||
new Mattermost(),
|
new Mattermost(),
|
||||||
|
new Nostr(),
|
||||||
new Ntfy(),
|
new Ntfy(),
|
||||||
new Octopush(),
|
new Octopush(),
|
||||||
new OneBot(),
|
new OneBot(),
|
||||||
new Opsgenie(),
|
new Opsgenie(),
|
||||||
new PagerDuty(),
|
new PagerDuty(),
|
||||||
|
new FlashDuty(),
|
||||||
new PagerTree(),
|
new PagerTree(),
|
||||||
new PromoSMS(),
|
new PromoSMS(),
|
||||||
new Pushbullet(),
|
new Pushbullet(),
|
||||||
@@ -113,7 +119,6 @@ class Notification {
|
|||||||
new GoAlert(),
|
new GoAlert(),
|
||||||
new ZohoCliq()
|
new ZohoCliq()
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
if (! item.name) {
|
if (! item.name) {
|
||||||
throw new Error("Notification provider without name");
|
throw new Error("Notification provider without name");
|
||||||
|
@@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
|
|||||||
if (!tlsInfo.valid) {
|
if (!tlsInfo.valid) {
|
||||||
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
|
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
|
||||||
badgeValues.message = "Bad Cert";
|
badgeValues.message = "Bad Cert";
|
||||||
badgeValues.color = badgeConstants.downColor;
|
badgeValues.color = downColor;
|
||||||
} else {
|
} else {
|
||||||
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
|
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
|
||||||
|
|
||||||
|
@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
|
|||||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
|
const { badgeConstants } = require("../config");
|
||||||
|
const { makeBadge } = require("badge-maker");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// overall status-page status badge
|
||||||
|
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
const slug = request.params.slug;
|
||||||
|
const statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
upColor = badgeConstants.defaultUpColor,
|
||||||
|
downColor = badgeConstants.defaultDownColor,
|
||||||
|
partialColor = "#F6BE00",
|
||||||
|
maintenanceColor = "#808080",
|
||||||
|
style = badgeConstants.defaultStyle
|
||||||
|
} = request.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let monitorIDList = await R.getCol(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND public = 1
|
||||||
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
let hasUp = false;
|
||||||
|
let hasDown = false;
|
||||||
|
let hasMaintenance = false;
|
||||||
|
|
||||||
|
for (let monitorID of monitorIDList) {
|
||||||
|
// retrieve the latest heartbeat
|
||||||
|
let beat = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// to be sure, when corresponding monitor not found
|
||||||
|
if (beat.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// handle status of beat
|
||||||
|
if (beat[0].status === 3) {
|
||||||
|
hasMaintenance = true;
|
||||||
|
} else if (beat[0].status === 2) {
|
||||||
|
// ignored
|
||||||
|
} else if (beat[0].status === 1) {
|
||||||
|
hasUp = true;
|
||||||
|
} else {
|
||||||
|
hasDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
if (!hasUp && !hasDown && !hasMaintenance) {
|
||||||
|
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||||
|
|
||||||
|
badgeValues.message = "N/A";
|
||||||
|
badgeValues.color = badgeConstants.naColor;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (hasMaintenance) {
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
|
badgeValues.color = maintenanceColor;
|
||||||
|
badgeValues.message = "Maintenance";
|
||||||
|
} else if (hasUp && !hasDown) {
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
|
badgeValues.color = upColor;
|
||||||
|
badgeValues.message = "Up";
|
||||||
|
} else if (hasUp && hasDown) {
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
|
badgeValues.color = partialColor;
|
||||||
|
badgeValues.message = "Degraded";
|
||||||
|
} else {
|
||||||
|
badgeValues.label = label ? label : "";
|
||||||
|
badgeValues.color = downColor;
|
||||||
|
badgeValues.message = "Down";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the svg based on given values
|
||||||
|
const svg = makeBadge(badgeValues);
|
||||||
|
|
||||||
|
response.type("image/svg+xml");
|
||||||
|
response.send(svg);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
sendHttpError(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
@@ -15,18 +15,25 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
|||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
// Check Node.js Version
|
// Check Node.js Version
|
||||||
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
const nodeVersion = process.versions.node;
|
||||||
const requiredVersion = 14;
|
|
||||||
|
// Get the required Node.js version from package.json
|
||||||
|
const requiredNodeVersions = require("../package.json").engines.node;
|
||||||
|
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
||||||
console.log(`Your Node.js version: ${nodeVersion}`);
|
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||||
|
|
||||||
// See more: https://github.com/louislam/uptime-kuma/issues/3138
|
const semver = require("semver");
|
||||||
if (nodeVersion >= 20) {
|
const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
|
||||||
console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
|
|
||||||
|
// Exit Uptime Kuma immediately if the Node.js version is banned
|
||||||
|
if (semver.satisfies(nodeVersion, bannedNodeVersions)) {
|
||||||
|
console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||||
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeVersion < requiredVersion) {
|
// Warning if the Node.js version is not in the support list, but it maybe still works
|
||||||
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
|
if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
|
||||||
process.exit(-1);
|
console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
@@ -42,6 +49,7 @@ if (! process.env.NODE_ENV) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||||
|
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||||
|
|
||||||
log.info("server", "Importing Node libraries");
|
log.info("server", "Importing Node libraries");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
@@ -148,6 +156,7 @@ const { generalSocketHandler } = require("./socket-handlers/general-socket-handl
|
|||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const apicache = require("./modules/apicache");
|
const apicache = require("./modules/apicache");
|
||||||
|
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@@ -160,12 +169,6 @@ app.use(function (req, res, next) {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Use for decode the auth object
|
|
||||||
* @type {null}
|
|
||||||
*/
|
|
||||||
let jwtSecret = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Setup Page
|
* Show Setup Page
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -213,6 +216,7 @@ let needSetup = false;
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.post("/test-webhook", async (request, response) => {
|
app.post("/test-webhook", async (request, response) => {
|
||||||
log.debug("test", request.headers);
|
log.debug("test", request.headers);
|
||||||
log.debug("test", request.body);
|
log.debug("test", request.body);
|
||||||
@@ -267,7 +271,7 @@ let needSetup = false;
|
|||||||
log.info("server", "Adding socket handler");
|
log.info("server", "Adding socket handler");
|
||||||
io.on("connection", async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
|
|
||||||
sendInfo(socket);
|
sendInfo(socket, true);
|
||||||
|
|
||||||
if (needSetup) {
|
if (needSetup) {
|
||||||
log.info("server", "Redirect to setup page");
|
log.info("server", "Redirect to setup page");
|
||||||
@@ -284,7 +288,7 @@ let needSetup = false;
|
|||||||
log.info("auth", `Login by token. IP=${clientIP}`);
|
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let decoded = jwt.verify(token, jwtSecret);
|
let decoded = jwt.verify(token, server.jwtSecret);
|
||||||
|
|
||||||
log.info("auth", "Username from JWT: " + decoded.username);
|
log.info("auth", "Username from JWT: " + decoded.username);
|
||||||
|
|
||||||
@@ -355,7 +359,7 @@ let needSetup = false;
|
|||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: jwt.sign({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
}, jwtSecret),
|
}, server.jwtSecret),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +389,7 @@ let needSetup = false;
|
|||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: jwt.sign({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
}, jwtSecret),
|
}, server.jwtSecret),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -637,9 +641,16 @@ let needSetup = false;
|
|||||||
let notificationIDList = monitor.notificationIDList;
|
let notificationIDList = monitor.notificationIDList;
|
||||||
delete monitor.notificationIDList;
|
delete monitor.notificationIDList;
|
||||||
|
|
||||||
|
// Ensure status code ranges are strings
|
||||||
|
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
|
||||||
|
throw new Error("Accepted status codes are not all strings");
|
||||||
|
}
|
||||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
delete monitor.accepted_statuscodes;
|
delete monitor.accepted_statuscodes;
|
||||||
|
|
||||||
|
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||||
|
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
|
|
||||||
bean.import(monitor);
|
bean.import(monitor);
|
||||||
bean.user_id = socket.userID;
|
bean.user_id = socket.userID;
|
||||||
|
|
||||||
@@ -650,7 +661,10 @@ let needSetup = false;
|
|||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
await server.sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
await startMonitor(socket.userID, bean.id);
|
|
||||||
|
if (monitor.active !== false) {
|
||||||
|
await startMonitor(socket.userID, bean.id);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
@@ -696,6 +710,11 @@ let needSetup = false;
|
|||||||
removeGroupChildren = true;
|
removeGroupChildren = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure status code ranges are strings
|
||||||
|
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
|
||||||
|
throw new Error("Accepted status codes are not all strings");
|
||||||
|
}
|
||||||
|
|
||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.description = monitor.description;
|
bean.description = monitor.description;
|
||||||
bean.parent = monitor.parent;
|
bean.parent = monitor.parent;
|
||||||
@@ -706,6 +725,12 @@ let needSetup = false;
|
|||||||
bean.headers = monitor.headers;
|
bean.headers = monitor.headers;
|
||||||
bean.basic_auth_user = monitor.basic_auth_user;
|
bean.basic_auth_user = monitor.basic_auth_user;
|
||||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||||
|
bean.timeout = monitor.timeout;
|
||||||
|
bean.oauth_client_id = monitor.oauth_client_id,
|
||||||
|
bean.oauth_client_secret = monitor.oauth_client_secret,
|
||||||
|
bean.oauth_auth_method = this.oauth_auth_method,
|
||||||
|
bean.oauth_token_url = monitor.oauth_token_url,
|
||||||
|
bean.oauth_scopes = monitor.oauth_scopes,
|
||||||
bean.tlsCa = monitor.tlsCa;
|
bean.tlsCa = monitor.tlsCa;
|
||||||
bean.tlsCert = monitor.tlsCert;
|
bean.tlsCert = monitor.tlsCert;
|
||||||
bean.tlsKey = monitor.tlsKey;
|
bean.tlsKey = monitor.tlsKey;
|
||||||
@@ -717,6 +742,7 @@ let needSetup = false;
|
|||||||
bean.maxretries = monitor.maxretries;
|
bean.maxretries = monitor.maxretries;
|
||||||
bean.port = parseInt(monitor.port);
|
bean.port = parseInt(monitor.port);
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
|
bean.invertKeyword = monitor.invertKeyword;
|
||||||
bean.ignoreTls = monitor.ignoreTls;
|
bean.ignoreTls = monitor.ignoreTls;
|
||||||
bean.expiryNotification = monitor.expiryNotification;
|
bean.expiryNotification = monitor.expiryNotification;
|
||||||
bean.upsideDown = monitor.upsideDown;
|
bean.upsideDown = monitor.upsideDown;
|
||||||
@@ -751,6 +777,14 @@ let needSetup = false;
|
|||||||
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||||
bean.radiusSecret = monitor.radiusSecret;
|
bean.radiusSecret = monitor.radiusSecret;
|
||||||
bean.httpBodyEncoding = monitor.httpBodyEncoding;
|
bean.httpBodyEncoding = monitor.httpBodyEncoding;
|
||||||
|
bean.expectedValue = monitor.expectedValue;
|
||||||
|
bean.jsonPath = monitor.jsonPath;
|
||||||
|
bean.kafkaProducerTopic = monitor.kafkaProducerTopic;
|
||||||
|
bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||||
|
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
|
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
|
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||||
|
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
|
|
||||||
@@ -905,6 +939,8 @@ let needSetup = false;
|
|||||||
delete server.monitorList[monitorID];
|
delete server.monitorList[monitorID];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||||
monitorID,
|
monitorID,
|
||||||
socket.userID,
|
socket.userID,
|
||||||
@@ -913,6 +949,10 @@ let needSetup = false;
|
|||||||
// Fix #2880
|
// Fix #2880
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Deleted Successfully.",
|
msg: "Deleted Successfully.",
|
||||||
@@ -1156,6 +1196,8 @@ let needSetup = false;
|
|||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||||
|
|
||||||
await setSettings("general", data);
|
await setSettings("general", data);
|
||||||
server.entryPage = data.entryPage;
|
server.entryPage = data.entryPage;
|
||||||
|
|
||||||
@@ -1166,6 +1208,12 @@ let needSetup = false;
|
|||||||
await server.setTimezone(data.serverTimezone);
|
await server.setTimezone(data.serverTimezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Chrome Executable is changed, need to reset the browser
|
||||||
|
if (previousChromeExecutable !== data.chromeExecutable) {
|
||||||
|
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
|
||||||
|
await resetChrome();
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Saved"
|
msg: "Saved"
|
||||||
@@ -1335,6 +1383,7 @@ let needSetup = false;
|
|||||||
|
|
||||||
// Define default values
|
// Define default values
|
||||||
let retryInterval = 0;
|
let retryInterval = 0;
|
||||||
|
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Only replace the default value with the backup file data for the specific version, where it appears the first time
|
Only replace the default value with the backup file data for the specific version, where it appears the first time
|
||||||
@@ -1360,6 +1409,7 @@ let needSetup = false;
|
|||||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||||
authWorkstation: monitorListData[i].authWorkstation,
|
authWorkstation: monitorListData[i].authWorkstation,
|
||||||
authDomain: monitorListData[i].authDomain,
|
authDomain: monitorListData[i].authDomain,
|
||||||
|
timeout,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
resendInterval: monitorListData[i].resendInterval || 0,
|
resendInterval: monitorListData[i].resendInterval || 0,
|
||||||
@@ -1367,13 +1417,14 @@ let needSetup = false;
|
|||||||
maxretries: monitorListData[i].maxretries,
|
maxretries: monitorListData[i].maxretries,
|
||||||
port: monitorListData[i].port,
|
port: monitorListData[i].port,
|
||||||
keyword: monitorListData[i].keyword,
|
keyword: monitorListData[i].keyword,
|
||||||
|
invertKeyword: monitorListData[i].invertKeyword,
|
||||||
ignoreTls: monitorListData[i].ignoreTls,
|
ignoreTls: monitorListData[i].ignoreTls,
|
||||||
upsideDown: monitorListData[i].upsideDown,
|
upsideDown: monitorListData[i].upsideDown,
|
||||||
maxredirects: monitorListData[i].maxredirects,
|
maxredirects: monitorListData[i].maxredirects,
|
||||||
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
||||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||||
notificationIDList: {},
|
notificationIDList: monitorListData[i].notificationIDList,
|
||||||
proxy_id: monitorListData[i].proxy_id || null,
|
proxy_id: monitorListData[i].proxy_id || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1560,6 +1611,8 @@ let needSetup = false;
|
|||||||
await shutdownFunction();
|
await shutdownFunction();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
|
||||||
server.httpServer.listen(port, hostname, () => {
|
server.httpServer.listen(port, hostname, () => {
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
log.info("server", `Listening on ${hostname}:${port}`);
|
log.info("server", `Listening on ${hostname}:${port}`);
|
||||||
@@ -1637,6 +1690,7 @@ async function afterLogin(socket, user) {
|
|||||||
socket.join(user.id);
|
socket.join(user.id);
|
||||||
|
|
||||||
let monitorList = await server.sendMonitorList(socket);
|
let monitorList = await server.sendMonitorList(socket);
|
||||||
|
sendInfo(socket);
|
||||||
server.sendMaintenanceList(socket);
|
server.sendMaintenanceList(socket);
|
||||||
sendNotificationList(socket);
|
sendNotificationList(socket);
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
@@ -1704,7 +1758,7 @@ async function initDatabase(testMode = false) {
|
|||||||
needSetup = true;
|
needSetup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecret = jwtSecretBean.value;
|
server.jwtSecret = jwtSecretBean.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
|
|||||||
const { sendInfo } = require("../client");
|
const { sendInfo } = require("../client");
|
||||||
const { checkLogin } = require("../util-server");
|
const { checkLogin } = require("../util-server");
|
||||||
const GameResolver = require("gamedig/lib/GameResolver");
|
const GameResolver = require("gamedig/lib/GameResolver");
|
||||||
|
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||||
|
|
||||||
let gameResolver = new GameResolver();
|
let gameResolver = new GameResolver();
|
||||||
let gameList = null;
|
let gameList = null;
|
||||||
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("testChrome", (executable, callback) => {
|
||||||
|
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||||
|
testChrome(executable).then((version) => {
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Found Chromium/Chrome. Version: " + version,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -162,6 +162,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
statusPage.footer_text = config.footerText;
|
statusPage.footer_text = config.footerText;
|
||||||
statusPage.custom_css = config.customCSS;
|
statusPage.custom_css = config.customCSS;
|
||||||
statusPage.show_powered_by = config.showPoweredBy;
|
statusPage.show_powered_by = config.showPoweredBy;
|
||||||
|
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
||||||
statusPage.modified_date = R.isoDateTime();
|
statusPage.modified_date = R.isoDateTime();
|
||||||
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
||||||
|
|
||||||
|
@@ -10,7 +10,9 @@ const util = require("util");
|
|||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
const childProcess = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||||
@@ -54,6 +56,12 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use for decode the auth object
|
||||||
|
* @type {null}
|
||||||
|
*/
|
||||||
|
jwtSecret = null;
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
@@ -91,11 +99,18 @@ class UptimeKumaServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Monitor Types
|
||||||
|
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||||
|
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initialise app after the database has been set up */
|
/** Initialise app after the database has been set up */
|
||||||
async initAfterDatabaseReady() {
|
async initAfterDatabaseReady() {
|
||||||
|
// Static
|
||||||
|
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||||
|
|
||||||
await CacheableDnsHttpAgent.update();
|
await CacheableDnsHttpAgent.update();
|
||||||
|
|
||||||
process.env.TZ = await this.getTimezone();
|
process.env.TZ = await this.getTimezone();
|
||||||
@@ -200,7 +215,7 @@ class UptimeKumaServer {
|
|||||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||||
*/
|
*/
|
||||||
static errorLog(error, outputToConsole = true) {
|
static errorLog(error, outputToConsole = true) {
|
||||||
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
const errorLogStream = fs.createWriteStream(path.join(Database.dataDir, "/error.log"), {
|
||||||
flags: "a"
|
flags: "a"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,9 +252,9 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||||
|| socket.client.conn.request.headers["x-real-ip"]
|
|| socket.client.conn.request.headers["x-real-ip"]
|
||||||
|| clientIP.replace(/^.*:/, "");
|
|| clientIP.replace(/^::ffff:/, "");
|
||||||
} else {
|
} else {
|
||||||
return clientIP.replace(/^.*:/, "");
|
return clientIP.replace(/^::ffff:/, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,13 +265,43 @@ class UptimeKumaServer {
|
|||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async getTimezone() {
|
async getTimezone() {
|
||||||
|
// From process.env.TZ
|
||||||
|
try {
|
||||||
|
if (process.env.TZ) {
|
||||||
|
this.checkTimezone(process.env.TZ);
|
||||||
|
return process.env.TZ;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("timezone", e.message + " in process.env.TZ");
|
||||||
|
}
|
||||||
|
|
||||||
let timezone = await Settings.get("serverTimezone");
|
let timezone = await Settings.get("serverTimezone");
|
||||||
if (timezone) {
|
|
||||||
return timezone;
|
// From Settings
|
||||||
} else if (process.env.TZ) {
|
try {
|
||||||
return process.env.TZ;
|
log.debug("timezone", "Using timezone from settings: " + timezone);
|
||||||
} else {
|
if (timezone) {
|
||||||
return dayjs.tz.guess();
|
this.checkTimezone(timezone);
|
||||||
|
return timezone;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("timezone", e.message + " in settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess
|
||||||
|
try {
|
||||||
|
let guess = dayjs.tz.guess();
|
||||||
|
log.debug("timezone", "Guessing timezone: " + guess);
|
||||||
|
if (guess) {
|
||||||
|
this.checkTimezone(guess);
|
||||||
|
return guess;
|
||||||
|
} else {
|
||||||
|
return "UTC";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Guess failed, fall back to UTC
|
||||||
|
log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
|
||||||
|
return "UTC";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +313,72 @@ class UptimeKumaServer {
|
|||||||
return dayjs().format("Z");
|
return dayjs().format("Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an error if the timezone is invalid
|
||||||
|
* @param timezone
|
||||||
|
*/
|
||||||
|
checkTimezone(timezone) {
|
||||||
|
try {
|
||||||
|
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Invalid timezone:" + timezone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current server timezone and environment variables
|
* Set the current server timezone and environment variables
|
||||||
* @param {string} timezone
|
* @param {string} timezone
|
||||||
*/
|
*/
|
||||||
async setTimezone(timezone) {
|
async setTimezone(timezone) {
|
||||||
|
this.checkTimezone(timezone);
|
||||||
await Settings.set("serverTimezone", timezone, "general");
|
await Settings.set("serverTimezone", timezone, "general");
|
||||||
process.env.TZ = timezone;
|
process.env.TZ = timezone;
|
||||||
dayjs.tz.setDefault(timezone);
|
dayjs.tz.setDefault(timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop the server */
|
/**
|
||||||
async stop() {
|
* TODO: Listen logic should be moved to here
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
this.startServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the server
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async stop() {
|
||||||
|
this.stopServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all system services (e.g. nscd)
|
||||||
|
* For now, only used in Docker
|
||||||
|
*/
|
||||||
|
startServices() {
|
||||||
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
|
try {
|
||||||
|
log.info("services", "Starting nscd");
|
||||||
|
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
||||||
|
} catch (e) {
|
||||||
|
log.info("services", "Failed to start nscd");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all system services
|
||||||
|
*/
|
||||||
|
stopServices() {
|
||||||
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
|
try {
|
||||||
|
log.info("services", "Stopping nscd");
|
||||||
|
childProcess.execSync("sudo service nscd stop");
|
||||||
|
} catch (e) {
|
||||||
|
log.info("services", "Failed to stop nscd");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,5 +386,6 @@ module.exports = {
|
|||||||
UptimeKumaServer
|
UptimeKumaServer
|
||||||
};
|
};
|
||||||
|
|
||||||
// Must be at the end
|
// Must be at the end to avoid circular dependencies
|
||||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||||
|
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||||
|
@@ -21,6 +21,8 @@ const grpc = require("@grpc/grpc-js");
|
|||||||
const protojs = require("protobufjs");
|
const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const redis = require("redis");
|
const redis = require("redis");
|
||||||
|
const oidc = require("openid-client");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
rfc2865: { file, attributes },
|
rfc2865: { file, attributes },
|
||||||
@@ -28,8 +30,11 @@ const {
|
|||||||
} = require("node-radius-utils");
|
} = require("node-radius-utils");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
const isWindows = process.platform === /^win/.test(process.platform);
|
// SASLOptions used in JSDoc
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const { Kafka, SASLOptions } = require("kafkajs");
|
||||||
|
|
||||||
|
const isWindows = process.platform === /^win/.test(process.platform);
|
||||||
/**
|
/**
|
||||||
* Init or reset JWT secret
|
* Init or reset JWT secret
|
||||||
* @returns {Promise<Bean>}
|
* @returns {Promise<Bean>}
|
||||||
@@ -49,6 +54,43 @@ exports.initJWTSecret = async () => {
|
|||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a jwt and returns the payload portion without verifying the jqt.
|
||||||
|
* @param {string} jwt The input jwt as a string
|
||||||
|
* @returns {Object} Decoded jwt payload object
|
||||||
|
*/
|
||||||
|
exports.decodeJwt = (jwt) => {
|
||||||
|
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a Access Token form a oidc/oauth2 provider
|
||||||
|
* @param {string} tokenEndpoint The token URI form the auth service provider
|
||||||
|
* @param {string} clientId The oidc/oauth application client id
|
||||||
|
* @param {string} clientSecret The oidc/oauth application client secret
|
||||||
|
* @param {string} scope The scope the for which the token should be issued for
|
||||||
|
* @param {string} authMethod The method on how to sent the credentials. Default client_secret_basic
|
||||||
|
* @returns {Promise<oidc.TokenSet>} TokenSet promise if the token request was successful
|
||||||
|
*/
|
||||||
|
exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSecret, scope, authMethod = "client_secret_basic") => {
|
||||||
|
const oauthProvider = new oidc.Issuer({ token_endpoint: tokenEndpoint });
|
||||||
|
let client = new oauthProvider.Client({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
token_endpoint_auth_method: authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increase default timeout and clock tolerance
|
||||||
|
client[oidc.custom.http_options] = () => ({ timeout: 10000 });
|
||||||
|
client[oidc.custom.clock_tolerance] = 5;
|
||||||
|
|
||||||
|
let grantParams = { grant_type: "client_credentials" };
|
||||||
|
if (scope) {
|
||||||
|
grantParams.scope = scope;
|
||||||
|
}
|
||||||
|
return await client.grant(grantParams);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send TCP request to specified hostname and port
|
* Send TCP request to specified hostname and port
|
||||||
* @param {string} hostname Hostname / address of machine
|
* @param {string} hostname Hostname / address of machine
|
||||||
@@ -196,6 +238,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitor Kafka using Producer
|
||||||
|
* @param {string} topic Topic name to produce into
|
||||||
|
* @param {string} message Message to produce
|
||||||
|
* @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}]
|
||||||
|
* Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and
|
||||||
|
* interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma"
|
||||||
|
* and ssl defaults to false)
|
||||||
|
* @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':'
|
||||||
|
* @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to
|
||||||
|
* {})
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options;
|
||||||
|
|
||||||
|
let connectedToKafka = false;
|
||||||
|
|
||||||
|
const timeoutID = setTimeout(() => {
|
||||||
|
log.debug("kafkaProducer", "KafkaProducer timeout triggered");
|
||||||
|
connectedToKafka = true;
|
||||||
|
reject(new Error("Timeout"));
|
||||||
|
}, interval * 1000 * 0.8);
|
||||||
|
|
||||||
|
if (saslOptions.mechanism === "None") {
|
||||||
|
saslOptions = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = new Kafka({
|
||||||
|
brokers: brokers,
|
||||||
|
clientId: clientId,
|
||||||
|
sasl: saslOptions,
|
||||||
|
retry: {
|
||||||
|
retries: 0,
|
||||||
|
},
|
||||||
|
ssl: ssl,
|
||||||
|
});
|
||||||
|
|
||||||
|
let producer = client.producer({
|
||||||
|
allowAutoTopicCreation: allowAutoTopicCreation,
|
||||||
|
retry: {
|
||||||
|
retries: 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
producer.connect().then(
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
producer.send({
|
||||||
|
topic: topic,
|
||||||
|
messages: [{
|
||||||
|
value: message,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
connectedToKafka = true;
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
resolve("Message sent successfully");
|
||||||
|
} catch (e) {
|
||||||
|
connectedToKafka = true;
|
||||||
|
producer.disconnect();
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(new Error("Error sending message: " + e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).catch(
|
||||||
|
(e) => {
|
||||||
|
connectedToKafka = true;
|
||||||
|
producer.disconnect();
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(new Error("Error in producer connection: " + e.message));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
producer.on("producer.network.request_timeout", (_) => {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(new Error("producer.network.request_timeout"));
|
||||||
|
});
|
||||||
|
|
||||||
|
producer.on("producer.disconnect", (_) => {
|
||||||
|
if (!connectedToKafka) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(new Error("producer.disconnect"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use NTLM Auth for a http request.
|
* Use NTLM Auth for a http request.
|
||||||
* @param {Object} options The http request options
|
* @param {Object} options The http request options
|
||||||
@@ -378,6 +508,7 @@ exports.mongodbPing = async function (connectionString) {
|
|||||||
* @param {string} callingStationId ID of calling station
|
* @param {string} callingStationId ID of calling station
|
||||||
* @param {string} secret Secret to use
|
* @param {string} secret Secret to use
|
||||||
* @param {number} [port=1812] Port to contact radius server on
|
* @param {number} [port=1812] Port to contact radius server on
|
||||||
|
* @param {number} [timeout=2500] Timeout for connection to use
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
exports.radius = function (
|
exports.radius = function (
|
||||||
@@ -388,10 +519,13 @@ exports.radius = function (
|
|||||||
callingStationId,
|
callingStationId,
|
||||||
secret,
|
secret,
|
||||||
port = 1812,
|
port = 1812,
|
||||||
|
timeout = 2500,
|
||||||
) {
|
) {
|
||||||
const client = new radiusClient({
|
const client = new radiusClient({
|
||||||
host: hostname,
|
host: hostname,
|
||||||
hostPort: port,
|
hostPort: port,
|
||||||
|
timeout: timeout,
|
||||||
|
retries: 1,
|
||||||
dictionaries: [ file ],
|
dictionaries: [ file ],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -403,6 +537,12 @@ exports.radius = function (
|
|||||||
[ attributes.CALLING_STATION_ID, callingStationId ],
|
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||||
[ attributes.CALLED_STATION_ID, calledStationId ],
|
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||||
],
|
],
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.response?.code) {
|
||||||
|
throw Error(error.response.code);
|
||||||
|
} else {
|
||||||
|
throw Error(error.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -413,12 +553,18 @@ exports.radius = function (
|
|||||||
exports.redisPingAsync = function (dsn) {
|
exports.redisPingAsync = function (dsn) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = redis.createClient({
|
const client = redis.createClient({
|
||||||
url: dsn,
|
url: dsn
|
||||||
});
|
});
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
|
if (client.isOpen) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
client.connect().then(() => {
|
client.connect().then(() => {
|
||||||
|
if (!client.isOpen) {
|
||||||
|
client.emit("error", new Error("connection isn't open"));
|
||||||
|
}
|
||||||
client.ping().then((res, err) => {
|
client.ping().then((res, err) => {
|
||||||
if (client.isOpen) {
|
if (client.isOpen) {
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
@@ -428,7 +574,7 @@ exports.redisPingAsync = function (dsn) {
|
|||||||
} else {
|
} else {
|
||||||
resolve(res);
|
resolve(res);
|
||||||
}
|
}
|
||||||
});
|
}).catch(error => reject(error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -574,7 +720,6 @@ exports.checkCertificate = function (res) {
|
|||||||
* @param {number} status The status code to check
|
* @param {number} status The status code to check
|
||||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||||
* @returns {boolean} True if status code within range, false otherwise
|
* @returns {boolean} True if status code within range, false otherwise
|
||||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
|
||||||
*/
|
*/
|
||||||
exports.checkStatusCode = function (status, acceptedCodes) {
|
exports.checkStatusCode = function (status, acceptedCodes) {
|
||||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||||
@@ -582,6 +727,11 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const codeRange of acceptedCodes) {
|
for (const codeRange of acceptedCodes) {
|
||||||
|
if (typeof codeRange !== "string") {
|
||||||
|
log.error("monitor", `Accepted status code not a string. ${codeRange} is of type ${typeof codeRange}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
|
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
|
||||||
if (codeRangeSplit.length === 1) {
|
if (codeRangeSplit.length === 1) {
|
||||||
if (status === codeRangeSplit[0]) {
|
if (status === codeRangeSplit[0]) {
|
||||||
@@ -592,7 +742,8 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid status code range");
|
log.error("monitor", `${codeRange} is not a valid status code range`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,3 +1052,13 @@ module.exports.grpcQuery = async (options) => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For unit test, export functions
|
||||||
|
if (process.env.TEST_BACKEND) {
|
||||||
|
module.exports.__test = {
|
||||||
|
parseCertificateInfo,
|
||||||
|
};
|
||||||
|
module.exports.__getPrivateFunction = (functionName) => {
|
||||||
|
return module.exports.__test[functionName];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -111,6 +111,10 @@ optgroup {
|
|||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
@@ -158,6 +162,26 @@ optgroup {
|
|||||||
background-color: #161B22;
|
background-color: #161B22;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-outline-normal {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 25px;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color;
|
||||||
|
border: 1px solid $dark-font-color2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
@@ -436,12 +460,11 @@ optgroup {
|
|||||||
.monitor-list {
|
.monitor-list {
|
||||||
&.scrollbar {
|
&.scrollbar {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100% - 65px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
&.scrollbar {
|
&.scrollbar {
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 97px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -69,6 +69,7 @@
|
|||||||
.multiselect__content-wrapper {
|
.multiselect__content-wrapper {
|
||||||
background-color: $dark-bg2;
|
background-color: $dark-bg2;
|
||||||
border-color: $dark-border-color;
|
border-color: $dark-border-color;
|
||||||
|
z-index: 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect--above .multiselect__content-wrapper {
|
.multiselect--above .multiselect__content-wrapper {
|
||||||
|
70
src/components/ActionSelect.vue
Normal file
70
src/components/ActionSelect.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
|
||||||
|
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
|
||||||
|
</select>
|
||||||
|
<a class="btn btn-outline-primary" @click="action()">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Generic select field with a customizable action on the right.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The value of the select field.
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the select field is enabled / disabled.
|
||||||
|
*/
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The icon displayed in the right button of the select field.
|
||||||
|
* Accepts a Font Awesome icon string identifier.
|
||||||
|
* @example "plus"
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The action to be performed when the button is clicked.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Send value update to parent on change.
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -22,78 +22,78 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
|
||||||
<label for="duration" class="form-label">{{ $t("Badge Duration") }}</label>
|
<label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
|
||||||
<input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required>
|
<input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
|
||||||
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
|
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
|
||||||
<input id="label" v-model="badge.label" type="text" class="form-control" required>
|
<input id="label" v-model="badge.label" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
|
||||||
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
|
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
|
||||||
<input id="prefix" v-model="badge.prefix" type="text" class="form-control" required>
|
<input id="prefix" v-model="badge.prefix" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
|
||||||
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
|
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
|
||||||
<input id="suffix" v-model="badge.suffix" type="text" class="form-control" required>
|
<input id="suffix" v-model="badge.suffix" type="text" placeholder="%" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
|
||||||
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
|
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
|
||||||
<input id="labelColor" v-model="badge.labelColor" type="text" class="form-control" required>
|
<input id="labelColor" v-model="badge.labelColor" type="text" placeholder="#555" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
|
||||||
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
|
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
|
||||||
<input id="color" v-model="badge.color" type="text" class="form-control" required>
|
<input id="color" v-model="badge.color" type="text" :placeholder="badgeConstants.defaultUpColor" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
|
||||||
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
|
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
|
||||||
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control" required>
|
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
|
||||||
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
|
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
|
||||||
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" class="form-control" required>
|
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" placeholder="h" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
|
||||||
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
|
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
|
||||||
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" required>
|
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" :placeholder="badgeConstants.defaultUpColor">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
|
||||||
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
|
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
|
||||||
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" required>
|
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" :placeholder="badgeConstants.defaultDownColor">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
|
||||||
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
|
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
|
||||||
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" required>
|
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" :placeholder="badgeConstants.defaultPendingColor">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
|
||||||
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
|
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
|
||||||
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" required>
|
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
|
||||||
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
|
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
|
||||||
<input id="warnColor" v-model="badge.warnColor" type="number" min="0" class="form-control" required>
|
<input id="warnColor" v-model="badge.warnColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
|
||||||
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
|
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
|
||||||
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" required>
|
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireWarnDays">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
|
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
|
||||||
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
|
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
|
||||||
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" required>
|
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -109,12 +109,16 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
|
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
|
||||||
<input id="value" v-model="badge.value" type="text" class="form-control" required>
|
<input id="value" v-model="badge.value" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 pt-3 d-flex justify-content-center">
|
||||||
|
<img :src="badgeURL" :alt="$t('Badge Preview')">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="push-url" class="form-label">{{ $t("Badge URL") }}</label>
|
<label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
|
||||||
<CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" />
|
<CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,6 +135,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import CopyableInput from "./CopyableInput.vue";
|
import CopyableInput from "./CopyableInput.vue";
|
||||||
|
import { default as serverConfig } from "../../server/config.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -224,7 +229,8 @@ export default {
|
|||||||
"color",
|
"color",
|
||||||
"labelColor",
|
"labelColor",
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
badgeConstants: serverConfig.badgeConstants,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
56
src/components/CreateGroupDialog.vue
Normal file
56
src/components/CreateGroupDialog.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ $t("New Group") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="confirm">
|
||||||
|
<div>
|
||||||
|
<label for="draftGroupName" class="form-label">{{ $t("Group Name") }}</label>
|
||||||
|
<input id="draftGroupName" v-model="groupName" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
{{ $t("Cancel") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" :disabled="groupName == '' || groupName == null" @click="confirm">
|
||||||
|
{{ $t("Confirm") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data: () => ({
|
||||||
|
modal: null,
|
||||||
|
groupName: null,
|
||||||
|
}),
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** Show the confirm dialog */
|
||||||
|
show() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
this.$emit("added", this.groupName);
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -5,15 +5,24 @@
|
|||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
|
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
||||||
|
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||||
|
>
|
||||||
|
<div>{{ timeSinceFirstBeat }} ago</div>
|
||||||
|
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
||||||
|
<div>{{ timeSinceLastBeat }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -56,8 +65,30 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the amount of beats of padding needed to fill the length of shortBeatList.
|
||||||
|
*
|
||||||
|
* @return {number} The amount of beats of padding needed to fill the length of shortBeatList.
|
||||||
|
*/
|
||||||
|
numPadding() {
|
||||||
|
if (!this.beatList) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let num = this.beatList.length - this.maxBeat;
|
||||||
|
|
||||||
|
if (this.move) {
|
||||||
|
num = num - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num > 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1 * num;
|
||||||
|
},
|
||||||
|
|
||||||
shortBeatList() {
|
shortBeatList() {
|
||||||
if (! this.beatList) {
|
if (!this.beatList) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +146,53 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the style object for positioning the time element.
|
||||||
|
* @return {Object} The style object containing the CSS properties for positioning the time element.
|
||||||
|
*/
|
||||||
|
timeStyle() {
|
||||||
|
return {
|
||||||
|
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the time elapsed since the first valid beat.
|
||||||
|
*
|
||||||
|
* @return {string} The time elapsed in minutes or hours.
|
||||||
|
*/
|
||||||
|
timeSinceFirstBeat() {
|
||||||
|
const firstValidBeat = this.shortBeatList.at(this.numPadding);
|
||||||
|
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
|
||||||
|
if (minutes > 60) {
|
||||||
|
return (minutes / 60).toFixed(0) + "h";
|
||||||
|
} else {
|
||||||
|
return minutes + "m";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the elapsed time since the last valid beat was registered.
|
||||||
|
*
|
||||||
|
* @return {string} The elapsed time in a minutes, hours or "now".
|
||||||
|
*/
|
||||||
|
timeSinceLastBeat() {
|
||||||
|
const lastValidBeat = this.shortBeatList.at(-1);
|
||||||
|
const seconds = dayjs().diff(dayjs.utc(lastValidBeat?.time), "seconds");
|
||||||
|
|
||||||
|
let tolerance = 60 * 2; // default for when monitorList not available
|
||||||
|
if (this.$root.monitorList[this.monitorId] != null) {
|
||||||
|
tolerance = this.$root.monitorList[this.monitorId].interval * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds < tolerance) {
|
||||||
|
return "now";
|
||||||
|
} else if (seconds < 60 * 60) {
|
||||||
|
return (seconds / 60).toFixed(0) + "m ago";
|
||||||
|
} else {
|
||||||
|
return (seconds / 60 / 60).toFixed(0) + "h ago";
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
beatList: {
|
beatList: {
|
||||||
@@ -133,14 +211,14 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
if (this.heartbeatList === null) {
|
if (this.heartbeatList === null) {
|
||||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
if (!(this.monitorId in this.$root.heartbeatList)) {
|
||||||
this.$root.heartbeatList[this.monitorId] = [];
|
this.$root.heartbeatList[this.monitorId] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.size === "small") {
|
if (this.size !== "big") {
|
||||||
this.beatWidth = 5;
|
this.beatWidth = 5;
|
||||||
this.beatHeight = 16;
|
this.beatHeight = 16;
|
||||||
this.beatMargin = 2;
|
this.beatMargin = 2;
|
||||||
@@ -151,11 +229,11 @@ export default {
|
|||||||
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
||||||
const actualMargin = this.beatMargin * window.devicePixelRatio;
|
const actualMargin = this.beatMargin * window.devicePixelRatio;
|
||||||
|
|
||||||
if (! Number.isInteger(actualWidth)) {
|
if (!Number.isInteger(actualWidth)) {
|
||||||
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! Number.isInteger(actualMargin)) {
|
if (!Number.isInteger(actualMargin)) {
|
||||||
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
|
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,4 +307,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connecting-line {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #ededed;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,36 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shadow-box mb-3" :style="boxStyle">
|
<div class="shadow-box mb-3" :style="boxStyle">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="placeholder"></div>
|
<div class="header-top">
|
||||||
<div class="search-wrapper">
|
<button class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
|
||||||
<a v-if="searchText == ''" class="search-icon">
|
{{ $t("Select") }}
|
||||||
<font-awesome-icon icon="search" />
|
</button>
|
||||||
</a>
|
|
||||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
<div class="placeholder"></div>
|
||||||
<font-awesome-icon icon="times" />
|
<div class="search-wrapper">
|
||||||
</a>
|
<a v-if="searchText == ''" class="search-icon">
|
||||||
<form>
|
<font-awesome-icon icon="search" />
|
||||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
|
</a>
|
||||||
</form>
|
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
</a>
|
||||||
|
<form>
|
||||||
|
<input
|
||||||
|
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-filter">
|
||||||
|
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selection Controls -->
|
||||||
|
<div v-if="selectMode" class="selection-controls px-2 pt-2">
|
||||||
|
<input
|
||||||
|
v-model="selectAll"
|
||||||
|
class="form-check-input select-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
|
||||||
|
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
|
||||||
|
|
||||||
|
<span v-if="selectedMonitorCount > 0">
|
||||||
|
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
|
||||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
<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>
|
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
|
<MonitorListItem
|
||||||
|
v-for="(item, index) in sortedMonitorList"
|
||||||
|
:key="index"
|
||||||
|
:monitor="item"
|
||||||
|
:showPathName="filtersActive"
|
||||||
|
:isSelectMode="selectMode"
|
||||||
|
:isSelected="isSelected"
|
||||||
|
:select="select"
|
||||||
|
:deselect="deselect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
|
||||||
|
{{ $t("pauseMonitorMsg") }}
|
||||||
|
</Confirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
import MonitorListItem from "../components/MonitorListItem.vue";
|
import MonitorListItem from "../components/MonitorListItem.vue";
|
||||||
|
import MonitorListFilter from "./MonitorListFilter.vue";
|
||||||
import { getMonitorRelativeURL } from "../util.ts";
|
import { getMonitorRelativeURL } from "../util.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
Confirm,
|
||||||
MonitorListItem,
|
MonitorListItem,
|
||||||
|
MonitorListFilter,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
/** Should the scrollbar be shown */
|
/** Should the scrollbar be shown */
|
||||||
@@ -41,7 +86,16 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
selectMode: false,
|
||||||
|
selectAll: false,
|
||||||
|
disableSelectAllWatcher: false,
|
||||||
|
selectedMonitors: {},
|
||||||
windowTop: 0,
|
windowTop: 0,
|
||||||
|
filterState: {
|
||||||
|
status: null,
|
||||||
|
active: null,
|
||||||
|
tags: null,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -63,31 +117,68 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a sorted list of monitors based on the applied filters and search text.
|
||||||
|
*
|
||||||
|
* @return {Array} The sorted list of monitors.
|
||||||
|
*/
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
// Simple filter by search text
|
result = result.filter(monitor => {
|
||||||
// finds monitor name, tag name or tag value
|
// filter by search text
|
||||||
if (this.searchText !== "") {
|
// finds monitor name, tag name or tag value
|
||||||
const loweredSearchText = this.searchText.toLowerCase();
|
let searchTextMatch = true;
|
||||||
result = result.filter(monitor => {
|
if (this.searchText !== "") {
|
||||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
const loweredSearchText = this.searchText.toLowerCase();
|
||||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
searchTextMatch =
|
||||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||||
});
|
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||||
} else {
|
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||||
result = result.filter(monitor => monitor.parent === null);
|
}
|
||||||
}
|
|
||||||
|
// filter by status
|
||||||
|
let statusMatch = true;
|
||||||
|
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||||
|
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||||
|
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||||
|
}
|
||||||
|
statusMatch = this.filterState.status.includes(monitor.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by active
|
||||||
|
let activeMatch = true;
|
||||||
|
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||||
|
activeMatch = this.filterState.active.includes(monitor.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by tags
|
||||||
|
let tagsMatch = true;
|
||||||
|
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||||
|
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||||
|
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||||
|
.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide children if not filtering
|
||||||
|
let showChild = true;
|
||||||
|
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
|
||||||
|
if (monitor.parent !== null) {
|
||||||
|
showChild = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
|
||||||
|
});
|
||||||
|
|
||||||
// Filter result by active state, weight and alphabetical
|
// Filter result by active state, weight and alphabetical
|
||||||
result.sort((m1, m2) => {
|
result.sort((m1, m2) => {
|
||||||
|
|
||||||
if (m1.active !== m2.active) {
|
if (m1.active !== m2.active) {
|
||||||
if (m1.active === 0) {
|
if (m1.active === false) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m2.active === 0) {
|
if (m2.active === false) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +198,67 @@ export default {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isDarkTheme() {
|
||||||
|
return document.body.classList.contains("dark");
|
||||||
|
},
|
||||||
|
|
||||||
|
monitorListStyle() {
|
||||||
|
let listHeaderHeight = 107;
|
||||||
|
|
||||||
|
if (this.selectMode) {
|
||||||
|
listHeaderHeight += 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"height": `calc(100% - ${listHeaderHeight}px)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedMonitorCount() {
|
||||||
|
return Object.keys(this.selectedMonitors).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if any filters are active.
|
||||||
|
*
|
||||||
|
* @return {boolean} True if any filter is active, false otherwise.
|
||||||
|
*/
|
||||||
|
filtersActive() {
|
||||||
|
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchText() {
|
||||||
|
for (let monitor of this.sortedMonitorList) {
|
||||||
|
if (!this.selectedMonitors[monitor.id]) {
|
||||||
|
if (this.selectAll) {
|
||||||
|
this.disableSelectAllWatcher = true;
|
||||||
|
this.selectAll = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectAll() {
|
||||||
|
if (!this.disableSelectAllWatcher) {
|
||||||
|
this.selectedMonitors = {};
|
||||||
|
|
||||||
|
if (this.selectAll) {
|
||||||
|
this.sortedMonitorList.forEach((item) => {
|
||||||
|
this.selectedMonitors[item.id] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.disableSelectAllWatcher = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectMode() {
|
||||||
|
if (!this.selectMode) {
|
||||||
|
this.selectAll = false;
|
||||||
|
this.selectedMonitors = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("scroll", this.onScroll);
|
window.addEventListener("scroll", this.onScroll);
|
||||||
@@ -134,7 +286,61 @@ export default {
|
|||||||
/** Clear the search bar */
|
/** Clear the search bar */
|
||||||
clearSearchText() {
|
clearSearchText() {
|
||||||
this.searchText = "";
|
this.searchText = "";
|
||||||
}
|
},
|
||||||
|
/**
|
||||||
|
* Update the MonitorList Filter
|
||||||
|
* @param {object} newFilter Object with new filter
|
||||||
|
*/
|
||||||
|
updateFilter(newFilter) {
|
||||||
|
this.filterState = newFilter;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Deselect a monitor
|
||||||
|
* @param {number} id ID of monitor
|
||||||
|
*/
|
||||||
|
deselect(id) {
|
||||||
|
delete this.selectedMonitors[id];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Select a monitor
|
||||||
|
* @param {number} id ID of monitor
|
||||||
|
*/
|
||||||
|
select(id) {
|
||||||
|
this.selectedMonitors[id] = true;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Determine if monitor is selected
|
||||||
|
* @param {number} id ID of monitor
|
||||||
|
* @returns {bool}
|
||||||
|
*/
|
||||||
|
isSelected(id) {
|
||||||
|
return id in this.selectedMonitors;
|
||||||
|
},
|
||||||
|
/** Disable select mode and reset selection */
|
||||||
|
cancelSelectMode() {
|
||||||
|
this.selectMode = false;
|
||||||
|
this.selectedMonitors = {};
|
||||||
|
},
|
||||||
|
/** Show dialog to confirm pause */
|
||||||
|
pauseDialog() {
|
||||||
|
this.$refs.confirmPause.show();
|
||||||
|
},
|
||||||
|
/** Pause each selected monitor */
|
||||||
|
pauseSelected() {
|
||||||
|
Object.keys(this.selectedMonitors)
|
||||||
|
.filter(id => this.$root.monitorList[id].active)
|
||||||
|
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id));
|
||||||
|
|
||||||
|
this.cancelSelectMode();
|
||||||
|
},
|
||||||
|
/** Resume each selected monitor */
|
||||||
|
resumeSelected() {
|
||||||
|
Object.keys(this.selectedMonitors)
|
||||||
|
.filter(id => !this.$root.monitorList[id].active)
|
||||||
|
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id));
|
||||||
|
|
||||||
|
this.cancelSelectMode();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -159,8 +365,6 @@ export default {
|
|||||||
margin: -10px;
|
margin: -10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
background-color: $dark-header-bg;
|
background-color: $dark-header-bg;
|
||||||
@@ -168,6 +372,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 770px) {
|
@media (max-width: 770px) {
|
||||||
.list-header {
|
.list-header {
|
||||||
margin: -20px;
|
margin: -20px;
|
||||||
@@ -217,4 +432,11 @@ export default {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-controls {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
284
src/components/MonitorListFilter.vue
Normal file
284
src/components/MonitorListFilter.vue
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-2 pt-2 d-flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:title="$t('Clear current filters')"
|
||||||
|
class="clear-filters-btn btn"
|
||||||
|
:class="{ 'active': numFiltersActive > 0}"
|
||||||
|
tabindex="0"
|
||||||
|
:disabled="numFiltersActive === 0"
|
||||||
|
@click="clearFilters"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="stream" />
|
||||||
|
<span v-if="numFiltersActive > 0" class="px-1 fw-bold">{{ numFiltersActive }}</span>
|
||||||
|
<font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
|
||||||
|
</button>
|
||||||
|
<MonitorListFilterDropdown
|
||||||
|
:filterActive="filterState.status?.length > 0"
|
||||||
|
>
|
||||||
|
<template #status>
|
||||||
|
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
|
||||||
|
<span v-else>
|
||||||
|
{{ $t('Status') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #dropdown>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(1)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<Status :status="1" />
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.up }}
|
||||||
|
<span v-if="filterState.status?.includes(1)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(0)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<Status :status="0" />
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.down }}
|
||||||
|
<span v-if="filterState.status?.includes(0)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(2)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<Status :status="2" />
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.pending }}
|
||||||
|
<span v-if="filterState.status?.includes(2)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(3)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<Status :status="3" />
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.maintenance }}
|
||||||
|
<span v-if="filterState.status?.includes(3)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</MonitorListFilterDropdown>
|
||||||
|
<MonitorListFilterDropdown :filterActive="filterState.active?.length > 0">
|
||||||
|
<template #status>
|
||||||
|
<span v-if="filterState.active?.length === 1">
|
||||||
|
<span v-if="filterState.active[0]">{{ $t("Running") }}</span>
|
||||||
|
<span v-else>{{ $t("filterActivePaused") }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ $t("filterActive") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #dropdown>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(true)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span>{{ $t("Running") }}</span>
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.active }}
|
||||||
|
<span v-if="filterState.active?.includes(true)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span>{{ $t("filterActivePaused") }}</span>
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ $root.stats.pause }}
|
||||||
|
<span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</MonitorListFilterDropdown>
|
||||||
|
<MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0">
|
||||||
|
<template #status>
|
||||||
|
<Tag
|
||||||
|
v-if="filterState.tags?.length === 1"
|
||||||
|
:item="tagsList.find(tag => tag.id === filterState.tags[0])"
|
||||||
|
:size="'sm'"
|
||||||
|
/>
|
||||||
|
<span v-else>
|
||||||
|
{{ $t('Tags') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #dropdown>
|
||||||
|
<li v-for="tag in tagsList" :key="tag.id">
|
||||||
|
<div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span><Tag :item="tag" :size="'sm'" /></span>
|
||||||
|
<span class="ps-3">
|
||||||
|
{{ getTaggedMonitorCount(tag) }}
|
||||||
|
<span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active">
|
||||||
|
<font-awesome-icon icon="check" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</MonitorListFilterDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MonitorListFilterDropdown from "./MonitorListFilterDropdown.vue";
|
||||||
|
import Status from "./Status.vue";
|
||||||
|
import Tag from "./Tag.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MonitorListFilterDropdown,
|
||||||
|
Status,
|
||||||
|
Tag,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
filterState: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "updateFilter" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tagsList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
numFiltersActive() {
|
||||||
|
let num = 0;
|
||||||
|
|
||||||
|
Object.values(this.filterState).forEach(item => {
|
||||||
|
if (item != null && item.length > 0) {
|
||||||
|
num += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getExistingTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleStatusFilter(status) {
|
||||||
|
let newFilter = {
|
||||||
|
...this.filterState
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.status == null) {
|
||||||
|
newFilter.status = [ status ];
|
||||||
|
} else {
|
||||||
|
if (newFilter.status.includes(status)) {
|
||||||
|
newFilter.status = newFilter.status.filter(item => item !== status);
|
||||||
|
} else {
|
||||||
|
newFilter.status.push(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit("updateFilter", newFilter);
|
||||||
|
},
|
||||||
|
toggleActiveFilter(active) {
|
||||||
|
let newFilter = {
|
||||||
|
...this.filterState
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.active == null) {
|
||||||
|
newFilter.active = [ active ];
|
||||||
|
} else {
|
||||||
|
if (newFilter.active.includes(active)) {
|
||||||
|
newFilter.active = newFilter.active.filter(item => item !== active);
|
||||||
|
} else {
|
||||||
|
newFilter.active.push(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit("updateFilter", newFilter);
|
||||||
|
},
|
||||||
|
toggleTagFilter(tag) {
|
||||||
|
let newFilter = {
|
||||||
|
...this.filterState
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.tags == null) {
|
||||||
|
newFilter.tags = [ tag.id ];
|
||||||
|
} else {
|
||||||
|
if (newFilter.tags.includes(tag.id)) {
|
||||||
|
newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
|
||||||
|
} else {
|
||||||
|
newFilter.tags.push(tag.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit("updateFilter", newFilter);
|
||||||
|
},
|
||||||
|
clearFilters() {
|
||||||
|
this.$emit("updateFilter", {
|
||||||
|
status: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getExistingTags() {
|
||||||
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.tagsList = res.tags;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getTaggedMonitorCount(tag) {
|
||||||
|
return Object.values(this.$root.monitorList).filter(monitor => {
|
||||||
|
return monitor.tags.find(monitorTag => monitorTag.tag_id === tag.id);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.clear-filters-btn {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color;
|
||||||
|
border: 1px solid $dark-font-color2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid $highlight;
|
||||||
|
background-color: $highlight-white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
124
src/components/MonitorListFilterDropdown.vue
Normal file
124
src/components/MonitorListFilterDropdown.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
|
||||||
|
<button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0">
|
||||||
|
<div class="px-1 d-flex align-items-center">
|
||||||
|
<slot name="status"></slot>
|
||||||
|
</div>
|
||||||
|
<span class="px-1">
|
||||||
|
<font-awesome-icon icon="angle-down" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<ul class="filter-dropdown-menu" :class="{ 'open': open }">
|
||||||
|
<slot name="dropdown"></slot>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
filterActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleFocusOut(e) {
|
||||||
|
if (e.relatedTarget != null && this.$el.contains(e.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
@import "../assets/app.scss";
|
||||||
|
|
||||||
|
.filter-dropdown-menu {
|
||||||
|
z-index: 100;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 5px 0 !important;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
margin: 0;
|
||||||
|
transform: translate(0, 36px);
|
||||||
|
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||||
|
visibility: hidden;
|
||||||
|
list-style: none;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
height: unset;
|
||||||
|
visibility: inherit;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 5px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:focus {
|
||||||
|
background: $highlight-white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
color: $dark-font-color;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: $dark-font-color;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
background-color: $highlight !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-dropdown-status {
|
||||||
|
@extend .btn-outline-normal;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid $highlight;
|
||||||
|
background-color: $highlight-white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-active {
|
||||||
|
color: $highlight;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,34 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
<div :style="depthMargin">
|
||||||
<div class="row">
|
<!-- Checkbox -->
|
||||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
<div v-if="isSelectMode" class="select-input-wrapper">
|
||||||
<div class="info" :style="depthMargin">
|
<input
|
||||||
<Uptime :monitor="monitor" type="24" :pill="true" />
|
class="form-check-input select-input"
|
||||||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
type="checkbox"
|
||||||
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
:aria-label="$t('Check/Uncheck')"
|
||||||
</span>
|
:checked="isSelected(monitor.id)"
|
||||||
{{ monitorName }}
|
@click.stop="toggleSelection"
|
||||||
</div>
|
/>
|
||||||
<div class="tags">
|
|
||||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||||
<div class="col-12 bottom-style">
|
<div class="row">
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.id" />
|
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||||
|
<div class="info">
|
||||||
|
<Uptime :monitor="monitor" type="24" :pill="true" />
|
||||||
|
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||||
|
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
||||||
|
</span>
|
||||||
|
{{ monitorName }}
|
||||||
|
</div>
|
||||||
|
<div v-if="monitor.tags.length > 0" class="tags">
|
||||||
|
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
|
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</router-link>
|
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||||
|
<div class="col-12 bottom-style">
|
||||||
|
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<transition name="slide-fade-up">
|
<transition name="slide-fade-up">
|
||||||
<div v-if="!isCollapsed" class="childs">
|
<div v-if="!isCollapsed" class="childs">
|
||||||
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
|
<MonitorListItem
|
||||||
|
v-for="(item, index) in sortedChildMonitorList"
|
||||||
|
:key="index" :monitor="item"
|
||||||
|
:showPathName="showPathName"
|
||||||
|
:isSelectMode="isSelectMode"
|
||||||
|
:isSelected="isSelected"
|
||||||
|
:select="select"
|
||||||
|
:deselect="deselect"
|
||||||
|
:depth="depth + 1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,8 +75,13 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
/** If the user is currently searching */
|
/** Should the monitor name show it's parent */
|
||||||
isSearch: {
|
showPathName: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/** If the user is in select mode */
|
||||||
|
isSelectMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
@@ -63,6 +90,21 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
/** Callback to determine if monitor is selected */
|
||||||
|
isSelected: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
/** Callback fired when monitor is selected */
|
||||||
|
select: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
/** Callback fired when monitor is deselected */
|
||||||
|
deselect: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -111,13 +153,19 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
monitorName() {
|
monitorName() {
|
||||||
if (this.isSearch) {
|
if (this.showPathName) {
|
||||||
return this.monitor.pathName;
|
return this.monitor.pathName;
|
||||||
} else {
|
} else {
|
||||||
return this.monitor.name;
|
return this.monitor.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
isSelectMode() {
|
||||||
|
// TODO: Resize the heartbeat bar, but too slow
|
||||||
|
// this.$refs.heartbeatBar.resize();
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
|
||||||
// Always unfold if monitor is accessed directly
|
// Always unfold if monitor is accessed directly
|
||||||
@@ -164,6 +212,16 @@ export default {
|
|||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return getMonitorRelativeURL(id);
|
return getMonitorRelativeURL(id);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Toggle selection of monitor
|
||||||
|
*/
|
||||||
|
toggleSelection() {
|
||||||
|
if (this.isSelected(this.monitor.id)) {
|
||||||
|
this.deselect(this.monitor.id);
|
||||||
|
} else {
|
||||||
|
this.select(this.monitor.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -201,4 +259,14 @@ export default {
|
|||||||
transition: all 0.2s $easing-in;
|
transition: all 0.2s $easing-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-input-wrapper {
|
||||||
|
float: left;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding-left: 4px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -104,7 +104,7 @@ export default {
|
|||||||
// We must check if there are any elements in monitorList to
|
// We must check if there are any elements in monitorList to
|
||||||
// prevent undefined errors if it hasn't been loaded yet
|
// prevent undefined errors if it hasn't been loaded yet
|
||||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||||
}
|
}
|
||||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||||
},
|
},
|
||||||
|
@@ -126,6 +126,7 @@ export default {
|
|||||||
"lunasea": "LunaSea",
|
"lunasea": "LunaSea",
|
||||||
"matrix": "Matrix",
|
"matrix": "Matrix",
|
||||||
"mattermost": "Mattermost",
|
"mattermost": "Mattermost",
|
||||||
|
"nostr": "Nostr",
|
||||||
"ntfy": "Ntfy",
|
"ntfy": "Ntfy",
|
||||||
"octopush": "Octopush",
|
"octopush": "Octopush",
|
||||||
"OneBot": "OneBot",
|
"OneBot": "OneBot",
|
||||||
@@ -157,6 +158,7 @@ export default {
|
|||||||
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
|
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
|
||||||
"DingDing": "DingDing (钉钉自定义机器人)",
|
"DingDing": "DingDing (钉钉自定义机器人)",
|
||||||
"Feishu": "Feishu (飞书)",
|
"Feishu": "Feishu (飞书)",
|
||||||
|
"FlashDuty": "FlashDuty (快猫星云)",
|
||||||
"FreeMobile": "FreeMobile (mobile.free.fr)",
|
"FreeMobile": "FreeMobile (mobile.free.fr)",
|
||||||
"PushDeer": "PushDeer",
|
"PushDeer": "PushDeer",
|
||||||
"promosms": "PromoSMS",
|
"promosms": "PromoSMS",
|
||||||
@@ -164,6 +166,7 @@ export default {
|
|||||||
"SMSManager": "SmsManager (smsmanager.cz)",
|
"SMSManager": "SmsManager (smsmanager.cz)",
|
||||||
"WeCom": "WeCom (企业微信群机器人)",
|
"WeCom": "WeCom (企业微信群机器人)",
|
||||||
"ServerChan": "ServerChan (Server酱)",
|
"ServerChan": "ServerChan (Server酱)",
|
||||||
|
"smsc": "SMSC",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort by notification name
|
// Sort by notification name
|
||||||
|
@@ -61,12 +61,17 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showTags" class="tags">
|
<div class="extra-info">
|
||||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
<div v-if="showCertificateExpiry && monitor.element.type === 'http'">
|
||||||
|
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
|
||||||
|
</div>
|
||||||
|
<div v-if="showTags">
|
||||||
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,6 +108,10 @@ export default {
|
|||||||
/** Should tags be shown? */
|
/** Should tags be shown? */
|
||||||
showTags: {
|
showTags: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
},
|
||||||
|
/** Should expiry be shown? */
|
||||||
|
showCertificateExpiry: {
|
||||||
|
type: Boolean,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -150,10 +159,37 @@ export default {
|
|||||||
// We must check if there are any elements in monitorList to
|
// We must check if there are any elements in monitorList to
|
||||||
// prevent undefined errors if it hasn't been loaded yet
|
// prevent undefined errors if it hasn't been loaded yet
|
||||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||||
}
|
}
|
||||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns formatted certificate expiry or Bad cert message
|
||||||
|
* @param {Object} monitor Monitor to show expiry for
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formattedCertExpiryMessage(monitor) {
|
||||||
|
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
|
||||||
|
return monitor.element.certExpiryDaysRemaining + " " + this.$tc("day", monitor.element.certExpiryDaysRemaining);
|
||||||
|
} else if (monitor?.element?.validCert === false) {
|
||||||
|
return this.$t("noOrBadCertificate");
|
||||||
|
} else {
|
||||||
|
return this.$t("Unknown") + " " + this.$tc("day", 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns certificate expiry based on days remaining
|
||||||
|
* @param {Object} monitor Monitor to show expiry for
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
certExpiryColor(monitor) {
|
||||||
|
if (monitor?.element?.validCert && monitor.element.certExpiryDaysRemaining > 7) {
|
||||||
|
return "#059669";
|
||||||
|
}
|
||||||
|
return "#DC2626";
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -161,6 +197,15 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars";
|
@import "../assets/vars";
|
||||||
|
|
||||||
|
.extra-info {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-info > div > div:first-child {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.no-monitor-msg {
|
.no-monitor-msg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
<button v-if="tag && tag.id !== null" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
{{ $t("Delete") }}
|
{{ $t("Delete") }}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
29
src/components/notifications/FlashDuty.vue
Normal file
29
src/components/notifications/FlashDuty.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="flashduty-integration-url" class="form-label">Integration Key</label>
|
||||||
|
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false"></HiddenInput>
|
||||||
|
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
|
||||||
|
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="flashduty-severity" class="form-label">{{ $t("FlashDuty Severity") }}</label>
|
||||||
|
<select id="flashduty-severity" v-model="$parent.notification.flashdutySeverity" class="form-select" :required="true">
|
||||||
|
<option value="Info" selected>Info</option>
|
||||||
|
<option value="Warning" selected>Warning</option>
|
||||||
|
<option value="Critical">Critical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
|
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
|
||||||
<div class="input-group mb-3">
|
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
|
||||||
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
|
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
|
||||||
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
|
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@@ -1,23 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<div class="input-group mb-3">
|
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
|
||||||
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
|
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<div class="input-group mb-3">
|
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
|
||||||
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span>
|
<label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
<select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
|
<select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
|
||||||
<option value="ios">iOS</option>
|
<option value="ios">iOS</option>
|
||||||
<option value="android">{{ $t("Android") }}</option>
|
<option value="android">Android</option>
|
||||||
<option value="huawei">{{ $t("Huawei") }}</option>
|
<option value="huawei">{{ $t("Huawei") }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,9 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
<div class="input-group mb-3">
|
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||||
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@@ -9,10 +9,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="kook-guild-id" class="form-label">{{ $t("Guild ID") }}</label>
|
<label for="kook-guild-id" class="form-label">{{ $t("Guild ID") }}</label>
|
||||||
|
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
<a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
<a href="https://developers.mattermost.com/integrate/webhooks/incoming/" target="_blank">https://developers.mattermost.com/integrate/webhooks/incoming/</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
{{ $t("aboutMattermostChannelName") }}
|
{{ $t("aboutMattermostChannelName") }}
|
||||||
|
26
src/components/notifications/Nostr.vue
Normal file
26
src/components/notifications/Nostr.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nostr-relays" class="form-label">{{ $t("nostrRelays") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<textarea id="nostr-relays" v-model="$parent.notification.relays" class="form-control" :required="true" placeholder="wss://127.0.0.1:7777/"></textarea>
|
||||||
|
<small class="form-text text-muted">{{ $t("nostrRelaysHelp") }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nostr-sender" class="form-label">{{ $t("nostrSender") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<HiddenInput id="nostr-sender" v-model="$parent.notification.sender" autocomplete="new-password" :required="true"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nostr-recipients" class="form-label">{{ $t("nostrRecipients") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<textarea id="nostr-recipients" v-model="$parent.notification.recipients" class="form-control" :required="true" placeholder="npub123... npub789..."></textarea>
|
||||||
|
<small class="form-text text-muted">{{ $t("nostrRecipientsHelp") }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
||||||
<div class="input-group mb-3">
|
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||||
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||||
<div class="input-group mb-3">
|
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
||||||
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
<div class="form-text">
|
||||||
|
{{ $t("Server URL should not contain the nfty topic") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -23,21 +22,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||||
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
|
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
|
||||||
<div class="input-group mb-3">
|
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
|
||||||
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||||
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
|
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
|
||||||
<div class="input-group mb-3">
|
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||||
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
|
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
|
||||||
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||||
<div class="input-group mb-3">
|
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
|
||||||
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
|
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushdeer-server" class="form-label">{{ $t("PushDeer Server URL") }}</label>
|
||||||
|
<input id="pushdeer-server" v-model="$parent.notification.pushdeerServer" type="text" class="form-control" placeholder="https://api2.pushdeer.com">
|
||||||
|
<div class="form-text">{{ $t("pushDeerServerDescription") }}</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
|
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
|
||||||
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
|
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
|
||||||
|
@@ -6,9 +6,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||||
<div class="input-group mb-3">
|
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||||
<a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
<a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||||
|
43
src/components/notifications/SMSC.vue
Normal file
43
src/components/notifications/SMSC.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsc-login" class="form-label">{{ $t("API Username") }}</label>
|
||||||
|
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
|
||||||
|
<a href="https://smsc.kz/" target="_blank">{{ $t("here") }}</a>
|
||||||
|
</i18n-t>
|
||||||
|
<input id="smsc-login" v-model="$parent.notification.smscLogin" type="text" class="form-control" required>
|
||||||
|
<label for="smsc-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
|
<HiddenInput id="smsc-key" v-model="$parent.notification.smscPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("checkPrice", ['СМСЦ']) }}
|
||||||
|
<a href="https://smsc.kz/tariffs/" target="_blank">https://smsc.kz/tariffs/</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsc-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
|
||||||
|
<input id="smsc-to-number" v-model="$parent.notification.smscToNumber" type="text" minlength="11" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsc-sender-name" class="form-label">{{ $t("From Name/Number") }}</label>
|
||||||
|
<input id="smsc-sender-name" v-model="$parent.notification.smscSenderName" type="text" minlength="1" maxlength="15" class="form-control">
|
||||||
|
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smsc-platform" class="form-label">{{ $t("smscTranslit") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||||
|
<select id="smsc-platform" v-model="$parent.notification.smscTranslit" class="form-select">
|
||||||
|
<option value="0">{{ $t("Default") }}</option>
|
||||||
|
<option value="1">Translit</option>
|
||||||
|
<option value="2">MpaHc/Ium</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@@ -24,5 +24,13 @@
|
|||||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input id="slack-channel-notify" v-model="$parent.notification.slackchannelnotify" type="checkbox" class="form-check-input">
|
||||||
|
<label for="slack-channel-notify" class="form-label">{{ $t("Notify Channel") }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("aboutNotifyChannel") }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -5,7 +5,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
|
<label for="twilio-apikey-token" class="form-label">{{ $t("Api Key (optional)") }}</label>
|
||||||
|
<input id="twilio-apikey-token" v-model="$parent.notification.twilioApiKey" type="text" class="form-control">
|
||||||
|
<div class="form-text">
|
||||||
|
<p>
|
||||||
|
The API key is optional but recommended. You can provide either Account SID and AuthToken
|
||||||
|
from the may TwilioConsole page or Account SID and the pair of Api Key and Api Key secret
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token / Api Key Secret") }}</label>
|
||||||
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
|
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -12,61 +12,97 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="webhook-content-type" class="form-label">{{
|
<label for="webhook-request-body" class="form-label">{{
|
||||||
$t("Content Type")
|
$t("Request Body")
|
||||||
}}</label>
|
}}</label>
|
||||||
<select
|
<select
|
||||||
id="webhook-content-type"
|
id="webhook-request-body"
|
||||||
v-model="$parent.notification.webhookContentType"
|
v-model="$parent.notification.webhookContentType"
|
||||||
class="form-select"
|
class="form-select"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="json">application/json</option>
|
<option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
|
||||||
<option value="form-data">multipart/form-data</option>
|
<option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
|
||||||
|
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
|
<div v-if="$parent.notification.webhookContentType == 'json'">
|
||||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
|
||||||
<template #multipart>"multipart/form-data"</template>
|
</div>
|
||||||
<template #decodeFunction>
|
<div v-if="$parent.notification.webhookContentType == 'form-data'">
|
||||||
<strong>json_decode($_POST['data'])</strong>
|
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||||
</template>
|
<template #multipart>multipart/form-data"</template>
|
||||||
</i18n-t>
|
<template #decodeFunction>
|
||||||
|
<strong>json_decode($_POST['data'])</strong>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div v-if="$parent.notification.webhookContentType == 'custom'">
|
||||||
|
<i18n-t tag="p" keypath="webhookCustomBodyDesc">
|
||||||
|
<template #msg>
|
||||||
|
<code>msg</code>
|
||||||
|
</template>
|
||||||
|
<template #heartbeat>
|
||||||
|
<code>heartbeatJSON</code>
|
||||||
|
</template>
|
||||||
|
<template #monitor>
|
||||||
|
<code>monitorJSON</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-if="$parent.notification.webhookContentType == 'custom'"
|
||||||
|
id="customBody"
|
||||||
|
v-model="$parent.notification.webhookCustomBody"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="customBodyPlaceholder"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<i18n-t
|
<div class="form-check form-switch">
|
||||||
tag="label"
|
<input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
|
||||||
class="form-label"
|
<label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
|
||||||
for="additionalHeaders"
|
</div>
|
||||||
keypath="webhookAdditionalHeadersTitle"
|
<div class="form-text">
|
||||||
>
|
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
|
||||||
</i18n-t>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
v-if="showAdditionalHeadersField"
|
||||||
id="additionalHeaders"
|
id="additionalHeaders"
|
||||||
v-model="$parent.notification.webhookAdditionalHeaders"
|
v-model="$parent.notification.webhookAdditionalHeaders"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="headersPlaceholder"
|
:placeholder="headersPlaceholder"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="form-text">
|
|
||||||
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
headersPlaceholder() {
|
headersPlaceholder() {
|
||||||
return this.$t("Example:", [
|
return this.$t("Example:", [
|
||||||
`
|
`
|
||||||
{
|
{
|
||||||
"HeaderName": "HeaderValue"
|
"Authorization": "Authorization Token"
|
||||||
}`,
|
}`,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
customBodyPlaceholder() {
|
||||||
|
return `Example:
|
||||||
|
{
|
||||||
|
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
|
||||||
|
"Body": "{{ msg }}"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
|
|||||||
import Apprise from "./Apprise.vue";
|
import Apprise from "./Apprise.vue";
|
||||||
import Bark from "./Bark.vue";
|
import Bark from "./Bark.vue";
|
||||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||||
|
import SMSC from "./SMSC.vue";
|
||||||
import DingDing from "./DingDing.vue";
|
import DingDing from "./DingDing.vue";
|
||||||
import Discord from "./Discord.vue";
|
import Discord from "./Discord.vue";
|
||||||
import Feishu from "./Feishu.vue";
|
import Feishu from "./Feishu.vue";
|
||||||
@@ -18,11 +19,13 @@ import LineNotify from "./LineNotify.vue";
|
|||||||
import LunaSea from "./LunaSea.vue";
|
import LunaSea from "./LunaSea.vue";
|
||||||
import Matrix from "./Matrix.vue";
|
import Matrix from "./Matrix.vue";
|
||||||
import Mattermost from "./Mattermost.vue";
|
import Mattermost from "./Mattermost.vue";
|
||||||
|
import Nostr from "./Nostr.vue";
|
||||||
import Ntfy from "./Ntfy.vue";
|
import Ntfy from "./Ntfy.vue";
|
||||||
import Octopush from "./Octopush.vue";
|
import Octopush from "./Octopush.vue";
|
||||||
import OneBot from "./OneBot.vue";
|
import OneBot from "./OneBot.vue";
|
||||||
import Opsgenie from "./Opsgenie.vue";
|
import Opsgenie from "./Opsgenie.vue";
|
||||||
import PagerDuty from "./PagerDuty.vue";
|
import PagerDuty from "./PagerDuty.vue";
|
||||||
|
import FlashDuty from "./FlashDuty.vue";
|
||||||
import PagerTree from "./PagerTree.vue";
|
import PagerTree from "./PagerTree.vue";
|
||||||
import PromoSMS from "./PromoSMS.vue";
|
import PromoSMS from "./PromoSMS.vue";
|
||||||
import Pushbullet from "./Pushbullet.vue";
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
@@ -61,6 +64,7 @@ const NotificationFormList = {
|
|||||||
"apprise": Apprise,
|
"apprise": Apprise,
|
||||||
"Bark": Bark,
|
"Bark": Bark,
|
||||||
"clicksendsms": ClickSendSMS,
|
"clicksendsms": ClickSendSMS,
|
||||||
|
"smsc": SMSC,
|
||||||
"DingDing": DingDing,
|
"DingDing": DingDing,
|
||||||
"discord": Discord,
|
"discord": Discord,
|
||||||
"Feishu": Feishu,
|
"Feishu": Feishu,
|
||||||
@@ -75,11 +79,13 @@ const NotificationFormList = {
|
|||||||
"lunasea": LunaSea,
|
"lunasea": LunaSea,
|
||||||
"matrix": Matrix,
|
"matrix": Matrix,
|
||||||
"mattermost": Mattermost,
|
"mattermost": Mattermost,
|
||||||
|
"nostr": Nostr,
|
||||||
"ntfy": Ntfy,
|
"ntfy": Ntfy,
|
||||||
"octopush": Octopush,
|
"octopush": Octopush,
|
||||||
"OneBot": OneBot,
|
"OneBot": OneBot,
|
||||||
"Opsgenie": Opsgenie,
|
"Opsgenie": Opsgenie,
|
||||||
"PagerDuty": PagerDuty,
|
"PagerDuty": PagerDuty,
|
||||||
|
"FlashDuty": FlashDuty,
|
||||||
"PagerTree": PagerTree,
|
"PagerTree": PagerTree,
|
||||||
"promosms": PromoSMS,
|
"promosms": PromoSMS,
|
||||||
"pushbullet": Pushbullet,
|
"pushbullet": Pushbullet,
|
||||||
|
@@ -112,6 +112,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeline -->
|
||||||
|
<div class="my-4">
|
||||||
|
<label class="form-label">{{ $t("styleElapsedTime") }}</label>
|
||||||
|
<div>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<input
|
||||||
|
id="styleElapsedTimeShowNoLine"
|
||||||
|
v-model="$root.styleElapsedTime"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="styleElapsedTime"
|
||||||
|
autocomplete="off"
|
||||||
|
value="no-line"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="styleElapsedTimeShowNoLine">
|
||||||
|
{{ $t("styleElapsedTimeShowNoLine") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="styleElapsedTimeShowWithLine"
|
||||||
|
v-model="$root.styleElapsedTime"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="styleElapsedTime"
|
||||||
|
autocomplete="off"
|
||||||
|
value="with-line"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="styleElapsedTimeShowWithLine">
|
||||||
|
{{ $t("styleElapsedTimeShowWithLine") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="styleElapsedTimeNone"
|
||||||
|
v-model="$root.styleElapsedTime"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="styleElapsedTime"
|
||||||
|
autocomplete="off"
|
||||||
|
value="none"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="styleElapsedTimeNone">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@@ -190,6 +190,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Chrome Executable -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="primaryBaseURL">
|
||||||
|
{{ $t("chromeExecutable") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
id="primaryBaseURL"
|
||||||
|
v-model="settings.chromeExecutable"
|
||||||
|
class="form-control"
|
||||||
|
name="primaryBaseURL"
|
||||||
|
:placeholder="$t('chromeExecutableAutoDetect')"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-outline-primary" type="button" @click="testChrome">
|
||||||
|
{{ $t("Test") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("chromeExecutableDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Save Button -->
|
<!-- Save Button -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
@@ -241,6 +265,12 @@ export default {
|
|||||||
autoGetPrimaryBaseURL() {
|
autoGetPrimaryBaseURL() {
|
||||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
testChrome() {
|
||||||
|
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@@ -455,8 +455,6 @@
|
|||||||
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
||||||
"Device Token": "رمز الجهاز",
|
"Device Token": "رمز الجهاز",
|
||||||
"Platform": "منصة",
|
"Platform": "منصة",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "ذكري المظهر",
|
|
||||||
"Huawei": "هواوي",
|
"Huawei": "هواوي",
|
||||||
"High": "عالٍ",
|
"High": "عالٍ",
|
||||||
"Retry": "إعادة المحاولة",
|
"Retry": "إعادة المحاولة",
|
||||||
|
@@ -592,7 +592,6 @@
|
|||||||
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
||||||
"Device Token": "رمز الجهاز",
|
"Device Token": "رمز الجهاز",
|
||||||
"Platform": "منصة",
|
"Platform": "منصة",
|
||||||
"Android": "ذكري المظهر",
|
|
||||||
"Huawei": "هواوي",
|
"Huawei": "هواوي",
|
||||||
"High": "عالٍ",
|
"High": "عالٍ",
|
||||||
"Retry": "إعادة المحاولة",
|
"Retry": "إعادة المحاولة",
|
||||||
|
@@ -333,7 +333,7 @@
|
|||||||
"Post": "Публикувай",
|
"Post": "Публикувай",
|
||||||
"Please input title and content": "Моля, въведете заглавие и съдържание",
|
"Please input title and content": "Моля, въведете заглавие и съдържание",
|
||||||
"Created": "Създаден",
|
"Created": "Създаден",
|
||||||
"Last Updated": "Последно обновен",
|
"Last Updated": "Последно обновена",
|
||||||
"Unpin": "Откачи",
|
"Unpin": "Откачи",
|
||||||
"Switch to Light Theme": "Превключи към светла тема",
|
"Switch to Light Theme": "Превключи към светла тема",
|
||||||
"Switch to Dark Theme": "Превключи към тъмна тема",
|
"Switch to Dark Theme": "Превключи към тъмна тема",
|
||||||
@@ -396,8 +396,6 @@
|
|||||||
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
|
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
|
||||||
"Device Token": "Токен за устройство",
|
"Device Token": "Токен за устройство",
|
||||||
"Platform": "Платформа",
|
"Platform": "Платформа",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "Висок",
|
"High": "Висок",
|
||||||
"Retry": "Повтори",
|
"Retry": "Повтори",
|
||||||
@@ -645,8 +643,8 @@
|
|||||||
"smseaglePriority": "Приоритет на съобщението (0-9, по подразбиране = 0)",
|
"smseaglePriority": "Приоритет на съобщението (0-9, по подразбиране = 0)",
|
||||||
"IconUrl": "Икона URL адрес",
|
"IconUrl": "Икона URL адрес",
|
||||||
"webhookAdditionalHeadersTitle": "Допълнителни хедъри",
|
"webhookAdditionalHeadersTitle": "Допълнителни хедъри",
|
||||||
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката.",
|
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката. Всеки хедър трябва да бъде дефиниран като JSON ключ/стойност.",
|
||||||
"Enable DNS Cache": "Активирай DNS кеширане",
|
"Enable DNS Cache": "Активирай DNS кеширане за HTTP(S) монитори",
|
||||||
"Enable": "Активирай",
|
"Enable": "Активирай",
|
||||||
"Disable": "Деактивирай",
|
"Disable": "Деактивирай",
|
||||||
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
|
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
|
||||||
@@ -740,7 +738,7 @@
|
|||||||
"lunaseaDeviceID": "ID на устройството",
|
"lunaseaDeviceID": "ID на устройството",
|
||||||
"lunaseaUserID": "ID на потребител",
|
"lunaseaUserID": "ID на потребител",
|
||||||
"twilioAccountSID": "Профил SID",
|
"twilioAccountSID": "Профил SID",
|
||||||
"twilioAuthToken": "Удостоверяващ токен",
|
"twilioAuthToken": "Удостоверяващ токен / Тайна на API ключа",
|
||||||
"twilioFromNumber": "От номер",
|
"twilioFromNumber": "От номер",
|
||||||
"twilioToNumber": "Към номер",
|
"twilioToNumber": "Към номер",
|
||||||
"sameAsServerTimezone": "Kато часовата зона на сървъра",
|
"sameAsServerTimezone": "Kато часовата зона на сървъра",
|
||||||
@@ -749,7 +747,7 @@
|
|||||||
"cronSchedule": "График: ",
|
"cronSchedule": "График: ",
|
||||||
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
|
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
|
||||||
"cronExpression": "Израз тип \"Cron\"",
|
"cronExpression": "Израз тип \"Cron\"",
|
||||||
"statusPageRefreshIn": "Обновяване след: {0}",
|
"statusPageRefreshIn": "Ще се обнови след: {0}",
|
||||||
"ntfyUsernameAndPassword": "Потребителско име и парола",
|
"ntfyUsernameAndPassword": "Потребителско име и парола",
|
||||||
"ntfyAuthenticationMethod": "Метод за удостоверяване",
|
"ntfyAuthenticationMethod": "Метод за удостоверяване",
|
||||||
"pushoverMessageTtl": "TTL на съобщението (секунди)",
|
"pushoverMessageTtl": "TTL на съобщението (секунди)",
|
||||||
@@ -757,7 +755,7 @@
|
|||||||
"Badge Generator": "Генератор на баджове на {0}",
|
"Badge Generator": "Генератор на баджове на {0}",
|
||||||
"Badge Type": "Тип бадж",
|
"Badge Type": "Тип бадж",
|
||||||
"Badge Duration": "Продължителност на баджа",
|
"Badge Duration": "Продължителност на баджа",
|
||||||
"Badge Prefix": "Префикс на баджа",
|
"Badge Prefix": "Префикс за стйността на баджа",
|
||||||
"Badge Label Color": "Цвят на етикета на баджа",
|
"Badge Label Color": "Цвят на етикета на баджа",
|
||||||
"Badge Color": "Цвят на баджа",
|
"Badge Color": "Цвят на баджа",
|
||||||
"Badge Label Suffix": "Суфикс на етикета на значката",
|
"Badge Label Suffix": "Суфикс на етикета на значката",
|
||||||
@@ -771,9 +769,9 @@
|
|||||||
"Badge URL": "URL адрес на баджа",
|
"Badge URL": "URL адрес на баджа",
|
||||||
"Monitor Setting": "Настройка на монитор {0}",
|
"Monitor Setting": "Настройка на монитор {0}",
|
||||||
"Show Clickable Link": "Покажи връзка, която може да се кликне",
|
"Show Clickable Link": "Покажи връзка, която може да се кликне",
|
||||||
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва URL адреса на монитора.",
|
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва мониторирания URL адрес.",
|
||||||
"Badge Label": "Етикет на баджа",
|
"Badge Label": "Етикет на баджа",
|
||||||
"Badge Suffix": "Суфикс на баджа",
|
"Badge Suffix": "Суфикс за стойността на баджа",
|
||||||
"Badge Label Prefix": "Префикс на етикета на значката",
|
"Badge Label Prefix": "Префикс на етикета на значката",
|
||||||
"Badge Pending Color": "Цвят на баджа за изчакващ",
|
"Badge Pending Color": "Цвят на баджа за изчакващ",
|
||||||
"Badge Down Days": "Колко дни баджът да не се показва",
|
"Badge Down Days": "Колко дни баджът да не се показва",
|
||||||
@@ -784,5 +782,56 @@
|
|||||||
"Edit Maintenance": "Редактиране на поддръжка",
|
"Edit Maintenance": "Редактиране на поддръжка",
|
||||||
"Home": "Главна страница",
|
"Home": "Главна страница",
|
||||||
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
|
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
|
||||||
"Close": "Затвори"
|
"Close": "Затвори",
|
||||||
|
"nostrRelays": "Nostr релета",
|
||||||
|
"nostrRelaysHelp": "Един URL адрес за реле на ред",
|
||||||
|
"nostrSender": "Частен ключ на изпращача (nsec)",
|
||||||
|
"nostrRecipients": "Публични ключове на получатели (npub)",
|
||||||
|
"nostrRecipientsHelp": "npub формат, по един на ред",
|
||||||
|
"chromeExecutable": "Chrome/Chromium изпълним файл",
|
||||||
|
"chromeExecutableAutoDetect": "Автоматично откриване",
|
||||||
|
"chromeExecutableDescription": "За потребителите на Docker, ако Chromium все още не е инсталиран, инсталирането и показването на резултата от теста може да отнеме няколко минути. Заема 1GB дисково пространство.",
|
||||||
|
"Invert Keyword": "Обърнат режим за ключова дума",
|
||||||
|
"invertKeywordDescription": "При търсене ключовата дума трябва да отсъства, а не да присъства.",
|
||||||
|
"webhookBodyPresetOption": "Предварителна настройка - {0}",
|
||||||
|
"webhookBodyCustomOption": "Персонализирано тяло",
|
||||||
|
"webhookCustomBodyDesc": "Дефинирайте персонализирано HTTP тяло за заявката. Приемат се шаблонни променливи {msg}, {heartbeat}, {monitor}.",
|
||||||
|
"Request Body": "Тяло на заявката",
|
||||||
|
"twilioApiKey": "API ключ (по избор)",
|
||||||
|
"Expected Value": "Очаквана стойност",
|
||||||
|
"Json Query": "Заявка тип JSON",
|
||||||
|
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте <a href='https://jsonata.org/'>jsonata.org</a> за документация относно езика на заявката. Имате възможност да тествате <a href='https://try.jsonata.org/'>тук</a>.",
|
||||||
|
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
|
||||||
|
"Badge Preview": "Преглед на баджа",
|
||||||
|
"Notify Channel": "Канал за известяване",
|
||||||
|
"aboutNotifyChannel": "Каналът за известяване ще задейства известие на настолен компютър или мобилно устройство за всички членове на канала, независимо дали тяхната наличност е в състояние активен или отсъстващ.",
|
||||||
|
"filterActive": "Активен",
|
||||||
|
"filterActivePaused": "На пауза",
|
||||||
|
"Kafka Brokers": "Kafka брокери",
|
||||||
|
"Enter the list of brokers": "Въведете списъка с брокери",
|
||||||
|
"Press Enter to add broker": "Натиснете Enter, за да добавите брокер",
|
||||||
|
"Kafka Topic Name": "Име на темата за Kafka",
|
||||||
|
"Enable Kafka SSL": "Активирай Kafka SSL",
|
||||||
|
"Enable Kafka Producer Auto Topic Creation": "Активирай автоматично създаване на темa в Kafka Producer",
|
||||||
|
"Kafka Producer Message": "Съобщение на Kafka Producer",
|
||||||
|
"Kafka SASL Options": "Опции на Kafka SASL",
|
||||||
|
"Mechanism": "Механизъм",
|
||||||
|
"Pick a SASL Mechanism...": "Изберете SASL механизъм...",
|
||||||
|
"Authorization Identity": "Идентичност за оторизиране",
|
||||||
|
"AccessKey Id": "AccessKey ID",
|
||||||
|
"Secret AccessKey": "Таен ключ за достъп",
|
||||||
|
"Session Token": "Токен за сесия",
|
||||||
|
"tailscalePingWarning": "За да използвате Tailscale Ping монитор, трябва да инсталирате Uptime Kuma без Docker и също така да инсталирате Tailscale клиент на вашия сървър.",
|
||||||
|
"Server URL should not contain the nfty topic": "URL адресът на сървъра не трябва да съдържа nfty темата",
|
||||||
|
"FlashDuty Severity": "Степен на тежест",
|
||||||
|
"showCertificateExpiry": "Показвай изтичащ сертификат",
|
||||||
|
"noOrBadCertificate": "Няма/лош сертификат",
|
||||||
|
"Select": "Избери",
|
||||||
|
"selectedMonitorCount": "Избрано: {0}",
|
||||||
|
"wayToGetFlashDutyKey": "Можете да отидете на страница 'Channel -> (Select a Channel) -> Integrations -> Add a new integration' и да добавите 'Custom Event', за да получите 'push' адрес и да копирате ключа за интегриране в адреса. За повече информация, моля посетете",
|
||||||
|
"PushDeer Server": "PushDeer сървър",
|
||||||
|
"pushDeerServerDescription": "Оставете празно, за да използвате официалния сървър",
|
||||||
|
"Check/Uncheck": "Постави/Премахни отметка",
|
||||||
|
"Request Timeout": "Време за изтичане на заявката",
|
||||||
|
"timeoutAfter": "Времето изтича след {0} секунди"
|
||||||
}
|
}
|
||||||
|
1
src/lang/bn.json
Normal file
1
src/lang/bn.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
@@ -18,13 +18,13 @@
|
|||||||
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
|
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
|
||||||
"Start of maintenance": "Zahájit údržbu",
|
"Start of maintenance": "Zahájit údržbu",
|
||||||
"All Status Pages": "Všechny stavové stránky",
|
"All Status Pages": "Všechny stavové stránky",
|
||||||
"Select status pages...": "Vyberte stavovou stránku…",
|
"Select status pages...": "Vyberte stavové stránky…",
|
||||||
"recurringIntervalMessage": "Spustit jednou každý den | Spustit jednou každých {0} dní",
|
"recurringIntervalMessage": "Spustit jednou každý den | Spustit jednou každých {0} dní",
|
||||||
"affectedMonitorsDescription": "Vyberte dohledy, které budou ovlivněny touto údržbou",
|
"affectedMonitorsDescription": "Vyberte dohledy, které budou ovlivněny touto údržbou",
|
||||||
"affectedStatusPages": "Zobrazit tuto zprávu o údržbě na vybraných stavových stránkách",
|
"affectedStatusPages": "Zobrazit tuto zprávu o údržbě na vybraných stavových stránkách",
|
||||||
"atLeastOneMonitor": "Vyberte alespoň jeden dotčený dohled",
|
"atLeastOneMonitor": "Vyberte alespoň jeden dotčený dohled",
|
||||||
"passwordNotMatchMsg": "Hesla se neshodují.",
|
"passwordNotMatchMsg": "Hesla se neshodují.",
|
||||||
"notificationDescription": "Aby byla upozornění fungovalo, je nutné ho přiřadit k dohledu.",
|
"notificationDescription": "Aby oznámení fungovala, je nutné jej přiřadit k dohledu.",
|
||||||
"keywordDescription": "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
|
"keywordDescription": "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
|
||||||
"pauseDashboardHome": "Pauza",
|
"pauseDashboardHome": "Pauza",
|
||||||
"deleteMonitorMsg": "Opravdu chcete odstranit tento dohled?",
|
"deleteMonitorMsg": "Opravdu chcete odstranit tento dohled?",
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
"Status Page": "Stavová stránka",
|
"Status Page": "Stavová stránka",
|
||||||
"Status Pages": "Stavová stránka",
|
"Status Pages": "Stavová stránka",
|
||||||
"defaultNotificationName": "Moje {notification} upozornění ({číslo})",
|
"defaultNotificationName": "Moje {notification} upozornění ({číslo})",
|
||||||
"here": "sem",
|
"here": "klikněte sem",
|
||||||
"Required": "Vyžadováno",
|
"Required": "Vyžadováno",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"ZohoCliq": "ZohoCliq",
|
"ZohoCliq": "ZohoCliq",
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
"webhookJsonDesc": "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js",
|
"webhookJsonDesc": "{0} je vhodný pro všechny moderní servery HTTP, jako je Express.js",
|
||||||
"webhookFormDataDesc": "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}",
|
"webhookFormDataDesc": "{multipart} je vhodné pro PHP. JSON bude nutné analyzovat prostřednictvím {decodeFunction}",
|
||||||
"webhookAdditionalHeadersTitle": "Dodatečné hlavičky",
|
"webhookAdditionalHeadersTitle": "Dodatečné hlavičky",
|
||||||
"webhookAdditionalHeadersDesc": "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem.",
|
"webhookAdditionalHeadersDesc": "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem. Každá hlavička by měla být definována jako klíč/hodnota v JSON.",
|
||||||
"smtp": "E-mail (SMTP)",
|
"smtp": "E-mail (SMTP)",
|
||||||
"secureOptionNone": "Žádné / STARTTLS (25, 587)",
|
"secureOptionNone": "Žádné / STARTTLS (25, 587)",
|
||||||
"secureOptionTLS": "TLS (465)",
|
"secureOptionTLS": "TLS (465)",
|
||||||
@@ -243,8 +243,8 @@
|
|||||||
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
|
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
|
||||||
"teams": "Microsoft Teams",
|
"teams": "Microsoft Teams",
|
||||||
"Webhook URL": "URL adresa webhooku",
|
"Webhook URL": "URL adresa webhooku",
|
||||||
"wayToGetTeamsURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
|
"wayToGetTeamsURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
|
||||||
"wayToGetZohoCliqURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
|
"wayToGetZohoCliqURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
|
||||||
"signal": "Signal",
|
"signal": "Signal",
|
||||||
"Number": "Číslo",
|
"Number": "Číslo",
|
||||||
"Recipients": "Příjemci",
|
"Recipients": "Příjemci",
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
"PasswordsDoNotMatch": "Hesla se neshodují.",
|
"PasswordsDoNotMatch": "Hesla se neshodují.",
|
||||||
"records": "záznamů",
|
"records": "záznamů",
|
||||||
"One record": "Jeden záznam",
|
"One record": "Jeden záznam",
|
||||||
"steamApiKeyDescription": "Pro monitorování Steam Game Serveru je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
|
"steamApiKeyDescription": "Pro monitorování herního serveru ve službě Steam je nutné zadat Steam Web-API klíč. Svůj API klíč získáte na následující stránce: ",
|
||||||
"Current User": "Aktuálně přihlášený uživatel",
|
"Current User": "Aktuálně přihlášený uživatel",
|
||||||
"topic": "Téma",
|
"topic": "Téma",
|
||||||
"topicExplanation": "MQTT téma, které chcete sledovat",
|
"topicExplanation": "MQTT téma, které chcete sledovat",
|
||||||
@@ -454,8 +454,6 @@
|
|||||||
"For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key",
|
"For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key",
|
||||||
"Device Token": "Token zařízení",
|
"Device Token": "Token zařízení",
|
||||||
"Platform": "Platforma",
|
"Platform": "Platforma",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "Vysoký",
|
"High": "Vysoký",
|
||||||
"Retry": "Opakovat",
|
"Retry": "Opakovat",
|
||||||
@@ -527,8 +525,8 @@
|
|||||||
"RadiusCalledStationIdDescription": "Identifikátor volaného zařízení",
|
"RadiusCalledStationIdDescription": "Identifikátor volaného zařízení",
|
||||||
"RadiusCallingStationId": "ID volajícího zařízení",
|
"RadiusCallingStationId": "ID volajícího zařízení",
|
||||||
"RadiusCallingStationIdDescription": "Identifikátor volajícího zařízení",
|
"RadiusCallingStationIdDescription": "Identifikátor volajícího zařízení",
|
||||||
"Certificate Expiry Notification": "Oznámení na blížící se konec platnosti certifikátu",
|
"Certificate Expiry Notification": "Upozornění na blížící se konec platnosti certifikátu",
|
||||||
"API Username": "Uživatelské jména API",
|
"API Username": "Uživatelské jméno API",
|
||||||
"API Key": "API klíč",
|
"API Key": "API klíč",
|
||||||
"Recipient Number": "Číslo příjemce",
|
"Recipient Number": "Číslo příjemce",
|
||||||
"From Name/Number": "Jméno/číslo odesílatele",
|
"From Name/Number": "Jméno/číslo odesílatele",
|
||||||
@@ -569,7 +567,7 @@
|
|||||||
"Also check beta release": "Kontrolovat také dostupnost beta verzí",
|
"Also check beta release": "Kontrolovat také dostupnost beta verzí",
|
||||||
"Using a Reverse Proxy?": "Používáte reverzní proxy?",
|
"Using a Reverse Proxy?": "Používáte reverzní proxy?",
|
||||||
"Check how to config it for WebSocket": "Zjistěte, jak ji nakonfigurovat pro WebSockety",
|
"Check how to config it for WebSocket": "Zjistěte, jak ji nakonfigurovat pro WebSockety",
|
||||||
"Steam Game Server": "Steam Game Server",
|
"Steam Game Server": "Herní server ve službě Steam",
|
||||||
"Most likely causes:": "Nejčastější důvody:",
|
"Most likely causes:": "Nejčastější důvody:",
|
||||||
"The resource is no longer available.": "Zdroj již není k dispozici.",
|
"The resource is no longer available.": "Zdroj již není k dispozici.",
|
||||||
"There might be a typing error in the address.": "Při zadávání adresy jste udělali chybu.",
|
"There might be a typing error in the address.": "Při zadávání adresy jste udělali chybu.",
|
||||||
@@ -640,7 +638,7 @@
|
|||||||
"dayOfWeek": "Den v týdnu",
|
"dayOfWeek": "Den v týdnu",
|
||||||
"dayOfMonth": "Den v měsíci",
|
"dayOfMonth": "Den v měsíci",
|
||||||
"lastDay": "Poslední den",
|
"lastDay": "Poslední den",
|
||||||
"lastDay1": "1. poslední den v měsíci",
|
"lastDay1": "Poslední den v měsíci",
|
||||||
"lastDay2": "2. poslední den v měsíci",
|
"lastDay2": "2. poslední den v měsíci",
|
||||||
"lastDay3": "3. poslední den v měsíci",
|
"lastDay3": "3. poslední den v měsíci",
|
||||||
"lastDay4": "4. poslední den v měsíci",
|
"lastDay4": "4. poslední den v měsíci",
|
||||||
@@ -655,10 +653,10 @@
|
|||||||
"Server Timezone": "Časové pásmo serveru",
|
"Server Timezone": "Časové pásmo serveru",
|
||||||
"statusPageMaintenanceEndDate": "Konec",
|
"statusPageMaintenanceEndDate": "Konec",
|
||||||
"IconUrl": "Adresa URL ikony",
|
"IconUrl": "Adresa URL ikony",
|
||||||
"Enable DNS Cache": "Povolit DNS Cache",
|
"Enable DNS Cache": "Povolit DNS Cache pro HTTP(s) dohledy",
|
||||||
"Enable": "Povolit",
|
"Enable": "Povolit",
|
||||||
"Disable": "Zakázat",
|
"Disable": "Zakázat",
|
||||||
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
|
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, tuto možnost vypněte.",
|
||||||
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
|
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
|
||||||
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
|
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
|
||||||
"Effective Date Range": "Časové období (volitelné)",
|
"Effective Date Range": "Časové období (volitelné)",
|
||||||
@@ -692,7 +690,7 @@
|
|||||||
"uninstall": "Odinstalace",
|
"uninstall": "Odinstalace",
|
||||||
"uninstalling": "Odinstalování",
|
"uninstalling": "Odinstalování",
|
||||||
"Packet Size": "Velikost paketu",
|
"Packet Size": "Velikost paketu",
|
||||||
"markdownSupported": "Markdown syntaxe podporována",
|
"markdownSupported": "Markdown syntaxe je podporována",
|
||||||
"Google Analytics ID": "ID Google Analytics",
|
"Google Analytics ID": "ID Google Analytics",
|
||||||
"Edit Tag": "Upravit štítek",
|
"Edit Tag": "Upravit štítek",
|
||||||
"Server Address": "Adresa serveru",
|
"Server Address": "Adresa serveru",
|
||||||
@@ -743,46 +741,97 @@
|
|||||||
"twilioAccountSID": "SID účtu",
|
"twilioAccountSID": "SID účtu",
|
||||||
"twilioFromNumber": "Číslo odesílatele",
|
"twilioFromNumber": "Číslo odesílatele",
|
||||||
"twilioToNumber": "Číslo příjemce",
|
"twilioToNumber": "Číslo příjemce",
|
||||||
"twilioAuthToken": "Autorizační token",
|
"twilioAuthToken": "Autorizační token / Tajemství API klíče",
|
||||||
"sameAsServerTimezone": "Stejné jako časové pásmo serveru",
|
"sameAsServerTimezone": "Stejné jako časové pásmo serveru",
|
||||||
"cronExpression": "Cron výraz",
|
"cronExpression": "Cron výraz",
|
||||||
"cronSchedule": "Plán: ",
|
"cronSchedule": "Plán: ",
|
||||||
"invalidCronExpression": "Neplatný cron výraz: {0}",
|
"invalidCronExpression": "Neplatný cron výraz: {0}",
|
||||||
"startDateTime": "Počáteční datum/čas",
|
"startDateTime": "Datum/čas začátku",
|
||||||
"endDateTime": "Datum/čas konce",
|
"endDateTime": "Datum/čas konce",
|
||||||
"ntfyAuthenticationMethod": "Způsob ověření",
|
"ntfyAuthenticationMethod": "Způsob ověření",
|
||||||
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
|
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
|
||||||
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
|
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
|
||||||
"Show Clickable Link": "Zobrazit klikatelný odkaz",
|
"Show Clickable Link": "Zobrazit klikatelný odkaz",
|
||||||
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
|
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL dohledu.",
|
||||||
"Open Badge Generator": "Otevřít generátor odznaků",
|
"Open Badge Generator": "Otevřít generátor odznaků",
|
||||||
"Badge Type": "Typ odznaku",
|
"Badge Type": "Typ odznaku",
|
||||||
"Badge Duration": "Platnost odznaku",
|
"Badge Duration": "Platnost odznaku",
|
||||||
"Badge Label": "Štítek odznaku",
|
"Badge Label": "Štítek odznaku",
|
||||||
"Badge Prefix": "Prefix odznaku",
|
"Badge Prefix": "Prefix hodnoty odznaku",
|
||||||
"Monitor Setting": "{0}'s Nastavení dohledu",
|
"Monitor Setting": "Nastavení dohledu pro {0}",
|
||||||
"Badge Generator": "Generátor odznaků pro {0}",
|
"Badge Generator": "Generátor odznaků pro {0}",
|
||||||
"Badge Label Color": "Barva štítku odznaku",
|
"Badge Label Color": "Barva štítku odznaku",
|
||||||
"Badge Color": "Barva odznaku",
|
"Badge Color": "Barva odznaku",
|
||||||
"Badge Style": "Styl odznaku",
|
"Badge Style": "Styl odznaku",
|
||||||
"Badge Label Suffix": "Přípona štítku odznaku",
|
"Badge Label Suffix": "Přípona štítku odznaku",
|
||||||
"Badge URL": "URL odznaku",
|
"Badge URL": "URL odznaku",
|
||||||
"Badge Suffix": "Přípona odznaku",
|
"Badge Suffix": "Přípona hodnoty odznaku",
|
||||||
"Badge Label Prefix": "Prefix štítku odznaku",
|
"Badge Label Prefix": "Prefix štítku odznaku",
|
||||||
"Badge Up Color": "Barva odznaku při Běží",
|
"Badge Up Color": "Barva odznaku při Běží",
|
||||||
"Badge Down Color": "Barva odznaku při Nedostupné",
|
"Badge Down Color": "Barva odznaku při Nedostupné",
|
||||||
"Badge Pending Color": "Barva odznaku při Pauze",
|
"Badge Pending Color": "Barva odznaku při Pauze",
|
||||||
"Badge Maintenance Color": "Barva odznaku při Údržbě",
|
"Badge Maintenance Color": "Barva odznaku při Údržbě",
|
||||||
"Badge Warn Color": "Barva odznaku při Upozornění",
|
"Badge Warn Color": "Barva odznaku při Upozornění",
|
||||||
"Reconnecting...": "Obnovení spojení...",
|
"Reconnecting...": "Obnovování spojení…",
|
||||||
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
|
"Cannot connect to the socket server": "Nelze se připojit k socketu serveru",
|
||||||
"Edit Maintenance": "Upravit Údržbu",
|
"Edit Maintenance": "Upravit Údržbu",
|
||||||
"Home": "Hlavní stránka",
|
"Home": "Hlavní stránka",
|
||||||
"Badge Down Days": "Odznak nedostupných dní",
|
"Badge Down Days": "Odznak nedostupných dní",
|
||||||
"Group": "Skupina",
|
"Group": "Skupina",
|
||||||
"Monitor Group": "Sledovaná skupina",
|
"Monitor Group": "Sledovaná skupina",
|
||||||
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
|
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupinu dohledů.",
|
||||||
"Close": "Zavřít",
|
"Close": "Zavřít",
|
||||||
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
|
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
|
||||||
"Badge Warn Days": "Odznak dní s upozorněním"
|
"Badge Warn Days": "Odznak dní s upozorněním",
|
||||||
|
"nostrSender": "Privátní klíč odesílatele (nsec)",
|
||||||
|
"nostrRelaysHelp": "Jedno relay URL na řádku",
|
||||||
|
"nostrRecipients": "Privátní klíče příjemců (npub)",
|
||||||
|
"nostrRecipientsHelp": "formát npub, jeden na řádku",
|
||||||
|
"chromeExecutable": "Spustitelný soubor Chrome/Chromium",
|
||||||
|
"chromeExecutableAutoDetect": "Automatická detekce",
|
||||||
|
"chromeExecutableDescription": "Pokud uživatelé nástroje Docker ještě nemají nainstalovanou aplikaci Chromium, může instalace a zobrazení výsledku testu trvat několik minut. Zabere 1 GB místa na disku.",
|
||||||
|
"Invert Keyword": "Inverzní klíčové slovo",
|
||||||
|
"webhookBodyPresetOption": "Uložená hodnota - {0}",
|
||||||
|
"webhookBodyCustomOption": "Vlastní tělo",
|
||||||
|
"invertKeywordDescription": "Hledá se klíčové slovo, které je spíše nepřítomné než přítomné.",
|
||||||
|
"webhookCustomBodyDesc": "Nastaví vlastní tělo HTTP pro požadavek. Akceptovány jsou proměnné {msg}, {heartbeat}, {monitor}.",
|
||||||
|
"Request Body": "Tělo požadavku",
|
||||||
|
"twilioApiKey": "Klíč k API (volitelný)",
|
||||||
|
"Expected Value": "Očekávaná hodnota",
|
||||||
|
"Json Query": "Json dotaz",
|
||||||
|
"Badge Duration (in hours)": "Zobrazení odznaku (v hodinách)",
|
||||||
|
"Badge Preview": "Náhled odznaku",
|
||||||
|
"Notify Channel": "Kanál nofitikací",
|
||||||
|
"aboutNotifyChannel": "Upozornění kanálu spustí upozornění na počítači nebo v mobilu pro všechny členy kanálu, ať už jsou dostupní nebo ne.",
|
||||||
|
"filterActive": "Aktivní",
|
||||||
|
"filterActivePaused": "Pozastaveno",
|
||||||
|
"Enter the list of brokers": "Vytvořte seznam zprostředkovatelů",
|
||||||
|
"Press Enter to add broker": "Stiskem klávesy Enter přidáte zprostředkovatele",
|
||||||
|
"Kafka Topic Name": "Název Kafka vlákna",
|
||||||
|
"Enable Kafka SSL": "Zapnout Kafka SSL",
|
||||||
|
"Mechanism": "Mechanismus",
|
||||||
|
"Kafka Brokers": "Kafka zprostředkovatelé",
|
||||||
|
"Authorization Identity": "Autorizační identita",
|
||||||
|
"AccessKey Id": "AccessKey Id",
|
||||||
|
"Session Token": "Token relace",
|
||||||
|
"Pick a SASL Mechanism...": "Vyberte SASL mechanismus…",
|
||||||
|
"Secret AccessKey": "Secret AccessKey",
|
||||||
|
"Server URL should not contain the nfty topic": "URL serveru by neměla obsahovat nfty vlákno",
|
||||||
|
"Kafka SASL Options": "Možnosti Kafka SASL",
|
||||||
|
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
|
||||||
|
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
|
||||||
|
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
|
||||||
|
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na <a href='https://jsonata.org/'>jsonata.org</a>, a využít můžete též <a href='https://try.jsonata.org/'>playground</a>.",
|
||||||
|
"Select": "Vybrat",
|
||||||
|
"selectedMonitorCount": "Vybráno: {0}",
|
||||||
|
"Check/Uncheck": "Vybrat/Zrušit výběr",
|
||||||
|
"showCertificateExpiry": "Zobrazit vypršení platnosti certifikátu",
|
||||||
|
"pushDeerServerDescription": "Chcete-li používat oficiální server, ponechte prázdné",
|
||||||
|
"noOrBadCertificate": "Žádný/Vadný certifikát",
|
||||||
|
"nostrRelays": "Relé Nostr",
|
||||||
|
"FlashDuty Severity": "Závažnost",
|
||||||
|
"PushDeer Server": "Server PushDeer",
|
||||||
|
"wayToGetFlashDutyKey": "Můžete přejít na stránku Kanál -> (Vyberte kanál) -> Integrace -> Přidat novou integraci, přidat \"Vlastní událost\" a získat adresu pro odeslání, zkopírovat klíč integrace do adresy. Další informace naleznete na adrese",
|
||||||
|
"Request Timeout": "Časový limit požadavku",
|
||||||
|
"timeoutAfter": "Vypršení časového limitu po {0} sekundách"
|
||||||
}
|
}
|
||||||
|
@@ -211,7 +211,7 @@
|
|||||||
"supportTelegramChatID": "Support Direct Chat / Group / Channel's Chat ID",
|
"supportTelegramChatID": "Support Direct Chat / Group / Channel's Chat ID",
|
||||||
"wayToGetTelegramChatID": "Du kan få dit chat-ID ved at sende en besked til bot'en og gå til denne URL for at se chat_id'et:",
|
"wayToGetTelegramChatID": "Du kan få dit chat-ID ved at sende en besked til bot'en og gå til denne URL for at se chat_id'et:",
|
||||||
"YOUR BOT TOKEN HERE": "DIT BOT TOKEN HER",
|
"YOUR BOT TOKEN HERE": "DIT BOT TOKEN HER",
|
||||||
"chatIDNotFound": "Chat-ID blev ikke fundet; send venligst en besked til denne bot først ",
|
"chatIDNotFound": "Chat-ID blev ikke fundet; send venligst en besked til denne bot først",
|
||||||
"Post URL": "Post URL",
|
"Post URL": "Post URL",
|
||||||
"Content Type": "Indholdstype",
|
"Content Type": "Indholdstype",
|
||||||
"webhookJsonDesc": "{0} er god til alle moderne HTTP-servere som f.eks Express.js",
|
"webhookJsonDesc": "{0} er god til alle moderne HTTP-servere som f.eks Express.js",
|
||||||
@@ -558,7 +558,6 @@
|
|||||||
"high": "høj",
|
"high": "høj",
|
||||||
"Base URL": "Base URL",
|
"Base URL": "Base URL",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"Retry": "Forsøg igen",
|
"Retry": "Forsøg igen",
|
||||||
"Topic": "Emne",
|
"Topic": "Emne",
|
||||||
|
@@ -48,7 +48,7 @@
|
|||||||
"Port": "Port",
|
"Port": "Port",
|
||||||
"Heartbeat Interval": "Prüfintervall",
|
"Heartbeat Interval": "Prüfintervall",
|
||||||
"Retries": "Wiederholungen",
|
"Retries": "Wiederholungen",
|
||||||
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
|
||||||
"Advanced": "Erweitert",
|
"Advanced": "Erweitert",
|
||||||
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||||
"Upside Down Mode": "Umgekehrter Modus",
|
"Upside Down Mode": "Umgekehrter Modus",
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
||||||
"Chat ID": "Chat ID",
|
"Chat ID": "Chat ID",
|
||||||
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
|
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
|
||||||
"wayToGetTelegramChatID": "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
|
"wayToGetTelegramChatID": "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id zu sehen",
|
||||||
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
|
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
|
||||||
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||||
"Post URL": "Post URL",
|
"Post URL": "Post URL",
|
||||||
@@ -403,8 +403,6 @@
|
|||||||
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
||||||
"Device Token": "Gerätetoken",
|
"Device Token": "Gerätetoken",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "Hoch",
|
"High": "Hoch",
|
||||||
"Retry": "Wiederholungen",
|
"Retry": "Wiederholungen",
|
||||||
@@ -657,10 +655,10 @@
|
|||||||
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
|
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
|
||||||
"markdownSupported": "Markdown-Syntax unterstützt",
|
"markdownSupported": "Markdown-Syntax unterstützt",
|
||||||
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
|
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
|
||||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden.",
|
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
|
||||||
"Packet Size": "Paketgrösse",
|
"Packet Size": "Paketgrösse",
|
||||||
"IconUrl": "Symbol URL",
|
"IconUrl": "Symbol URL",
|
||||||
"Enable DNS Cache": "DNS Cache aktivieren",
|
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
|
||||||
"Help": "Hilfe",
|
"Help": "Hilfe",
|
||||||
"Game": "Spiel",
|
"Game": "Spiel",
|
||||||
"General Monitor Type": "Allgemeiner Monitortyp",
|
"General Monitor Type": "Allgemeiner Monitortyp",
|
||||||
@@ -745,7 +743,7 @@
|
|||||||
"twilioAccountSID": "Account SID",
|
"twilioAccountSID": "Account SID",
|
||||||
"twilioFromNumber": "Absender",
|
"twilioFromNumber": "Absender",
|
||||||
"twilioToNumber": "Empfänger",
|
"twilioToNumber": "Empfänger",
|
||||||
"twilioAuthToken": "Auth Token",
|
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||||
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
||||||
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
||||||
"startDateTime": "Start Datum/Uhrzeit",
|
"startDateTime": "Start Datum/Uhrzeit",
|
||||||
@@ -758,8 +756,8 @@
|
|||||||
"Badge Type": "Badge Typ",
|
"Badge Type": "Badge Typ",
|
||||||
"Badge Duration": "Badge Dauer",
|
"Badge Duration": "Badge Dauer",
|
||||||
"Badge Label": "Badge Label",
|
"Badge Label": "Badge Label",
|
||||||
"Badge Prefix": "Badge Präfix",
|
"Badge Prefix": "Badge Wert Präfix",
|
||||||
"Badge Suffix": "Badge Suffix",
|
"Badge Suffix": "Badge Wert Suffix",
|
||||||
"Badge Label Color": "Badge Label Farbe",
|
"Badge Label Color": "Badge Label Farbe",
|
||||||
"Badge Color": "Badge Farbe",
|
"Badge Color": "Badge Farbe",
|
||||||
"Badge Label Prefix": "Badge Label Präfix",
|
"Badge Label Prefix": "Badge Label Präfix",
|
||||||
@@ -781,5 +779,41 @@
|
|||||||
"Group": "Gruppe",
|
"Group": "Gruppe",
|
||||||
"Monitor Group": "Monitor Gruppe",
|
"Monitor Group": "Monitor Gruppe",
|
||||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||||
"Close": "Schliessen"
|
"Close": "Schliessen",
|
||||||
|
"chromeExecutableAutoDetect": "Automatische Erkennung",
|
||||||
|
"chromeExecutableDescription": "Für Docker-Benutzer, die Chromium noch nicht installiert haben, kann es ein paar Minuten dauern, bis es installiert ist und das Testergebnis angezeigt wird. Es benötigt 1 GB Speicherplatz.",
|
||||||
|
"chromeExecutable": "Chrome/Chromium Ausführbare Datei",
|
||||||
|
"Invert Keyword": "Schlüsselwort invertieren",
|
||||||
|
"webhookCustomBodyDesc": "Definiere einen benutzerdefinierten HTTP-Body für die Anfrage. Die Template-Variablen {msg}, {heartbeat} und {monitor} werden akzeptiert.",
|
||||||
|
"webhookBodyPresetOption": "Voreinstellung - {0}",
|
||||||
|
"webhookBodyCustomOption": "Benutzerdefinierter Body",
|
||||||
|
"invertKeywordDescription": "Achte darauf, dass das Schlüsselwort eher fehlt als vorhanden ist.",
|
||||||
|
"Request Body": "Anforderungstext",
|
||||||
|
"twilioApiKey": "API-Schlüssel (optional)",
|
||||||
|
"aboutNotifyChannel": "Notify Kanal löst eine Desktop- oder Mobilbenachrichtigung für alle Mitglieder des Kanals aus, unabhängig davon, ob deine Verfügbarkeit auf aktiv oder abwesend eingestellt ist.",
|
||||||
|
"Notify Channel": "Notify Kanal",
|
||||||
|
"Enter the list of brokers": "Gib die Liste der Broker ein",
|
||||||
|
"Kafka Topic Name": "Kafka Topic Name",
|
||||||
|
"Kafka Producer Message": "Kafka Producer Nachricht",
|
||||||
|
"Enable Kafka SSL": "Kafka SSL aktivieren",
|
||||||
|
"Enable Kafka Producer Auto Topic Creation": "Kafka Producer Auto Topic Creation aktivieren",
|
||||||
|
"Kafka SASL Options": "Kafka SASL Optionen",
|
||||||
|
"Mechanism": "Mechanismus",
|
||||||
|
"Pick a SASL Mechanism...": "Wähle ein SASL Mechanismus...",
|
||||||
|
"AccessKey Id": "AccessKey Id",
|
||||||
|
"Secret AccessKey": "Secret AccessKey",
|
||||||
|
"Session Token": "Sitzungs-Token",
|
||||||
|
"Kafka Brokers": "Kafka Brokers",
|
||||||
|
"Press Enter to add broker": "Drücke Enter um den Broker hinzuzufügen",
|
||||||
|
"Authorization Identity": "Authorization Identity",
|
||||||
|
"Expected Value": "Erwarteter Wert",
|
||||||
|
"Json Query": "Json-Abfrage",
|
||||||
|
"filterActive": "Aktiv",
|
||||||
|
"filterActivePaused": "Pausiert",
|
||||||
|
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf <a href='https://jsonata.org/'>jsonata.org</a> findest du die Dokumentation zur Abfragesprache. <a href='https://try.jsonata.org/'>Hier</a> kannst du Abfragen üben.",
|
||||||
|
"Badge Duration (in hours)": "Badge Dauer (in Stunden)",
|
||||||
|
"Badge Preview": "Badge Vorschau",
|
||||||
|
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||||
|
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
||||||
|
"pushDeerServerDescription": "Leer lassen, um den offiziellen Server zu verwenden"
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@
|
|||||||
"Port": "Port",
|
"Port": "Port",
|
||||||
"Heartbeat Interval": "Prüfintervall",
|
"Heartbeat Interval": "Prüfintervall",
|
||||||
"Retries": "Wiederholungen",
|
"Retries": "Wiederholungen",
|
||||||
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
|
||||||
"Advanced": "Erweitert",
|
"Advanced": "Erweitert",
|
||||||
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||||
"Upside Down Mode": "Umgekehrter Modus",
|
"Upside Down Mode": "Umgekehrter Modus",
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
||||||
"Chat ID": "Chat ID",
|
"Chat ID": "Chat ID",
|
||||||
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
|
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
|
||||||
"wayToGetTelegramChatID": "Du kannst deine Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
|
"wayToGetTelegramChatID": "Du kannst deine Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id zu sehen:",
|
||||||
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
|
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
|
||||||
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||||
"Post URL": "Post URL",
|
"Post URL": "Post URL",
|
||||||
@@ -380,7 +380,7 @@
|
|||||||
"alertaAlertState": "Alarmstatus",
|
"alertaAlertState": "Alarmstatus",
|
||||||
"alertaRecoverState": "Wiederherstellungsstatus",
|
"alertaRecoverState": "Wiederherstellungsstatus",
|
||||||
"deleteStatusPageMsg": "Bist du sicher, dass du diese Status-Seite löschen willst?",
|
"deleteStatusPageMsg": "Bist du sicher, dass du diese Status-Seite löschen willst?",
|
||||||
"Proxies": "Proxies",
|
"Proxies": "Proxys",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"enabled": "Aktiviert",
|
"enabled": "Aktiviert",
|
||||||
"setAsDefault": "Als Standard setzen",
|
"setAsDefault": "Als Standard setzen",
|
||||||
@@ -403,8 +403,6 @@
|
|||||||
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
||||||
"Device Token": "Gerätetoken",
|
"Device Token": "Gerätetoken",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "Hoch",
|
"High": "Hoch",
|
||||||
"Retry": "Wiederholungen",
|
"Retry": "Wiederholungen",
|
||||||
@@ -646,11 +644,11 @@
|
|||||||
"Help": "Hilfe",
|
"Help": "Hilfe",
|
||||||
"Game": "Spiel",
|
"Game": "Spiel",
|
||||||
"Custom": "Benutzerdefiniert",
|
"Custom": "Benutzerdefiniert",
|
||||||
"Enable DNS Cache": "DNS-Cache aktivieren",
|
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
|
||||||
"Enable": "Aktivieren",
|
"Enable": "Aktivieren",
|
||||||
"Disable": "Deaktivieren",
|
"Disable": "Deaktivieren",
|
||||||
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
|
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
|
||||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.",
|
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
|
||||||
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
|
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
|
||||||
"loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
|
"loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
|
||||||
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
|
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
|
||||||
@@ -702,7 +700,7 @@
|
|||||||
"Edit Tag": "bearbeite Tag",
|
"Edit Tag": "bearbeite Tag",
|
||||||
"Server Address": "Server Adresse",
|
"Server Address": "Server Adresse",
|
||||||
"Learn More": "Erfahre mehr",
|
"Learn More": "Erfahre mehr",
|
||||||
"Body Encoding": "Körperkodierung",
|
"Body Encoding": "Inhaltskodierung",
|
||||||
"Add API Key": "API Schlüssel hinzufügen",
|
"Add API Key": "API Schlüssel hinzufügen",
|
||||||
"apiKey-active": "Aktiv",
|
"apiKey-active": "Aktiv",
|
||||||
"apiKey-expired": "Abgelaufen",
|
"apiKey-expired": "Abgelaufen",
|
||||||
@@ -749,7 +747,7 @@
|
|||||||
"twilioAccountSID": "Account SID",
|
"twilioAccountSID": "Account SID",
|
||||||
"twilioFromNumber": "Absender",
|
"twilioFromNumber": "Absender",
|
||||||
"twilioToNumber": "Empfänger",
|
"twilioToNumber": "Empfänger",
|
||||||
"twilioAuthToken": "Auth Token",
|
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||||
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
||||||
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
||||||
"startDateTime": "Start Datum/Uhrzeit",
|
"startDateTime": "Start Datum/Uhrzeit",
|
||||||
@@ -778,11 +776,47 @@
|
|||||||
"Badge Pending Color": "Badge Pending Farbe",
|
"Badge Pending Color": "Badge Pending Farbe",
|
||||||
"Badge Down Days": "Badge Down Tage",
|
"Badge Down Days": "Badge Down Tage",
|
||||||
"Monitor Setting": "{0}'s Monitor Einstellung",
|
"Monitor Setting": "{0}'s Monitor Einstellung",
|
||||||
"Badge Prefix": "Badge Präfix",
|
"Badge Prefix": "Badge Wert Präfix",
|
||||||
"Badge Suffix": "Badge Suffix",
|
"Badge Suffix": "Badge Wert Suffix",
|
||||||
"Badge Warn Days": "Badge Warnung Tage",
|
"Badge Warn Days": "Badge Warnung Tage",
|
||||||
"Group": "Gruppe",
|
"Group": "Gruppe",
|
||||||
"Monitor Group": "Monitor Gruppe",
|
"Monitor Group": "Monitor Gruppe",
|
||||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||||
"Close": "Schließen"
|
"Close": "Schließen",
|
||||||
|
"chromeExecutableAutoDetect": "Automatische Erkennung",
|
||||||
|
"chromeExecutableDescription": "Für Docker-Benutzer, die Chromium noch nicht installiert haben, kann es ein paar Minuten dauern, bis es installiert ist und das Testergebnis angezeigt wird. Es benötigt 1 GB Speicherplatz.",
|
||||||
|
"chromeExecutable": "Chrome/Chromium Ausführbare Datei",
|
||||||
|
"Invert Keyword": "Schlüsselwort invertieren",
|
||||||
|
"invertKeywordDescription": "Achte darauf, dass das Schlüsselwort eher fehlt als vorhanden ist.",
|
||||||
|
"webhookCustomBodyDesc": "Definiere einen benutzerdefinierten HTTP-Body für die Anfrage. Die Template-Variablen {msg}, {heartbeat} und {monitor} werden akzeptiert.",
|
||||||
|
"webhookBodyPresetOption": "Voreinstellung - {0}",
|
||||||
|
"webhookBodyCustomOption": "Benutzerdefinierter Body",
|
||||||
|
"Request Body": "Anforderungstext",
|
||||||
|
"Badge Duration (in hours)": "Badge Dauer (in Stunden)",
|
||||||
|
"Badge Preview": "Badge Vorschau",
|
||||||
|
"twilioApiKey": "API-Schlüssel (optional)",
|
||||||
|
"Notify Channel": "Notify Kanal",
|
||||||
|
"Enter the list of brokers": "Gib die Liste der Broker ein",
|
||||||
|
"Kafka Topic Name": "Kafka Topic Name",
|
||||||
|
"Kafka Producer Message": "Kafka Producer Nachricht",
|
||||||
|
"Enable Kafka SSL": "Kafka SSL aktivieren",
|
||||||
|
"Enable Kafka Producer Auto Topic Creation": "Kafka Producer Auto Topic Creation aktivieren",
|
||||||
|
"Kafka SASL Options": "Kafka SASL Optionen",
|
||||||
|
"Mechanism": "Mechanismus",
|
||||||
|
"Pick a SASL Mechanism...": "Wähle ein SASL Mechanismus...",
|
||||||
|
"Authorization Identity": "Authorization Identity",
|
||||||
|
"AccessKey Id": "AccessKey Id",
|
||||||
|
"Secret AccessKey": "Secret AccessKey",
|
||||||
|
"Session Token": "Sitzungs-Token",
|
||||||
|
"aboutNotifyChannel": "Notify Kanal löst eine Desktop- oder Mobilbenachrichtigung für alle Mitglieder des Kanals aus, unabhängig davon, ob deine Verfügbarkeit auf aktiv oder abwesend eingestellt ist.",
|
||||||
|
"Kafka Brokers": "Kafka Brokers",
|
||||||
|
"Press Enter to add broker": "Drücke Enter um den Broker hinzuzufügen",
|
||||||
|
"filterActive": "Aktiv",
|
||||||
|
"filterActivePaused": "Pausiert",
|
||||||
|
"Expected Value": "Erwarteter Wert",
|
||||||
|
"Json Query": "Json-Abfrage",
|
||||||
|
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf <a href='https://jsonata.org/'>jsonata.org</a> findest du die Dokumentation zur Abfragesprache. <a href='https://try.jsonata.org/'>Hier</a> kannst du Abfragen üben.",
|
||||||
|
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
|
||||||
|
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
|
||||||
|
"pushDeerServerDescription": "Leer lassen, um den offiziellen Server zu verwenden"
|
||||||
}
|
}
|
||||||
|
@@ -420,8 +420,6 @@
|
|||||||
"For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
|
"For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
|
||||||
"Device Token": "Device Token",
|
"Device Token": "Device Token",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "High",
|
"High": "High",
|
||||||
"Retry": "Ξαναδοκιμάσετε",
|
"Retry": "Ξαναδοκιμάσετε",
|
||||||
|
@@ -51,11 +51,16 @@
|
|||||||
"Ping": "Ping",
|
"Ping": "Ping",
|
||||||
"Monitor Type": "Monitor Type",
|
"Monitor Type": "Monitor Type",
|
||||||
"Keyword": "Keyword",
|
"Keyword": "Keyword",
|
||||||
|
"Invert Keyword": "Invert Keyword",
|
||||||
|
"Expected Value": "Expected Value",
|
||||||
|
"Json Query": "Json Query",
|
||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Friendly Name",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Hostname": "Hostname",
|
"Hostname": "Hostname",
|
||||||
"Port": "Port",
|
"Port": "Port",
|
||||||
"Heartbeat Interval": "Heartbeat Interval",
|
"Heartbeat Interval": "Heartbeat Interval",
|
||||||
|
"Request Timeout": "Request Timeout",
|
||||||
|
"timeoutAfter": "Timeout after {0} seconds",
|
||||||
"Retries": "Retries",
|
"Retries": "Retries",
|
||||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
"Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively",
|
"Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively",
|
||||||
@@ -82,6 +87,9 @@
|
|||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
"Auto": "Auto",
|
"Auto": "Auto",
|
||||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||||
|
"styleElapsedTime": "Elapsed time under the heartbeat bar",
|
||||||
|
"styleElapsedTimeShowNoLine": "Show (No Line)",
|
||||||
|
"styleElapsedTimeShowWithLine": "Show (With Line)",
|
||||||
"Normal": "Normal",
|
"Normal": "Normal",
|
||||||
"Bottom": "Bottom",
|
"Bottom": "Bottom",
|
||||||
"None": "None",
|
"None": "None",
|
||||||
@@ -152,6 +160,8 @@
|
|||||||
"Disable 2FA": "Disable 2FA",
|
"Disable 2FA": "Disable 2FA",
|
||||||
"2FA Settings": "2FA Settings",
|
"2FA Settings": "2FA Settings",
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
"Two Factor Authentication": "Two Factor Authentication",
|
||||||
|
"filterActive": "Active",
|
||||||
|
"filterActivePaused": "Paused",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"Inactive": "Inactive",
|
"Inactive": "Inactive",
|
||||||
"Token": "Token",
|
"Token": "Token",
|
||||||
@@ -195,8 +205,11 @@
|
|||||||
"Content Type": "Content Type",
|
"Content Type": "Content Type",
|
||||||
"webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
|
"webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
|
||||||
"webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
|
"webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
|
||||||
|
"webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
|
||||||
"webhookAdditionalHeadersTitle": "Additional Headers",
|
"webhookAdditionalHeadersTitle": "Additional Headers",
|
||||||
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook.",
|
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
|
||||||
|
"webhookBodyPresetOption": "Preset - {0}",
|
||||||
|
"webhookBodyCustomOption": "Custom Body",
|
||||||
"Webhook URL": "Webhook URL",
|
"Webhook URL": "Webhook URL",
|
||||||
"Application Token": "Application Token",
|
"Application Token": "Application Token",
|
||||||
"Server URL": "Server URL",
|
"Server URL": "Server URL",
|
||||||
@@ -261,6 +274,9 @@
|
|||||||
"Services": "Services",
|
"Services": "Services",
|
||||||
"Discard": "Discard",
|
"Discard": "Discard",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
|
"Select": "Select",
|
||||||
|
"selectedMonitorCount": "Selected: {0}",
|
||||||
|
"Check/Uncheck": "Check/Uncheck",
|
||||||
"Powered by": "Powered by",
|
"Powered by": "Powered by",
|
||||||
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||||
"Customize": "Customize",
|
"Customize": "Customize",
|
||||||
@@ -356,6 +372,7 @@
|
|||||||
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
||||||
"socket": "Socket",
|
"socket": "Socket",
|
||||||
"tcp": "TCP / HTTP",
|
"tcp": "TCP / HTTP",
|
||||||
|
"tailscalePingWarning": "In order to use the Tailscale Ping monitor, you need to install Uptime Kuma without Docker and also install Tailscale client on your server.",
|
||||||
"Docker Container": "Docker Container",
|
"Docker Container": "Docker Container",
|
||||||
"Container Name / ID": "Container Name / ID",
|
"Container Name / ID": "Container Name / ID",
|
||||||
"Docker Host": "Docker Host",
|
"Docker Host": "Docker Host",
|
||||||
@@ -432,9 +449,12 @@
|
|||||||
"Server Timezone": "Server Timezone",
|
"Server Timezone": "Server Timezone",
|
||||||
"statusPageMaintenanceEndDate": "End",
|
"statusPageMaintenanceEndDate": "End",
|
||||||
"IconUrl": "Icon URL",
|
"IconUrl": "Icon URL",
|
||||||
"Enable DNS Cache": "Enable DNS Cache",
|
"Enable DNS Cache": "Enable DNS Cache for HTTP(s) monitors",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
"Disable": "Disable",
|
"Disable": "Disable",
|
||||||
|
"chromeExecutable": "Chrome/Chromium Executable",
|
||||||
|
"chromeExecutableAutoDetect": "Auto Detect",
|
||||||
|
"chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
|
||||||
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
||||||
"Single Maintenance Window": "Single Maintenance Window",
|
"Single Maintenance Window": "Single Maintenance Window",
|
||||||
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
||||||
@@ -515,6 +535,8 @@
|
|||||||
"passwordNotMatchMsg": "The repeat password does not match.",
|
"passwordNotMatchMsg": "The repeat password does not match.",
|
||||||
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
||||||
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||||
|
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
||||||
|
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out <a href='https://jsonata.org/'>jsonata.org</a> for the documentation about the query language. A playground can be found <a href='https://try.jsonata.org/'>here</a>.",
|
||||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||||
"backupDescription2": "Note: history and event data is not included.",
|
"backupDescription2": "Note: history and event data is not included.",
|
||||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||||
@@ -606,7 +628,6 @@
|
|||||||
"For safety, must use secret key": "For safety, must use secret key",
|
"For safety, must use secret key": "For safety, must use secret key",
|
||||||
"Device Token": "Device Token",
|
"Device Token": "Device Token",
|
||||||
"Platform": "Platform",
|
"Platform": "Platform",
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "High",
|
"High": "High",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
@@ -629,6 +650,8 @@
|
|||||||
"matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
|
"matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
|
||||||
"matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
|
"matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
|
||||||
"Channel Name": "Channel Name",
|
"Channel Name": "Channel Name",
|
||||||
|
"Notify Channel": "Notify Channel",
|
||||||
|
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Icon Emoji",
|
||||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||||
@@ -675,12 +698,15 @@
|
|||||||
"Octopush API Version": "Octopush API Version",
|
"Octopush API Version": "Octopush API Version",
|
||||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
||||||
"ntfy Topic": "ntfy Topic",
|
"ntfy Topic": "ntfy Topic",
|
||||||
|
"Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic",
|
||||||
"onebotHttpAddress": "OneBot HTTP Address",
|
"onebotHttpAddress": "OneBot HTTP Address",
|
||||||
"onebotMessageType": "OneBot Message Type",
|
"onebotMessageType": "OneBot Message Type",
|
||||||
"onebotGroupMessage": "Group",
|
"onebotGroupMessage": "Group",
|
||||||
"onebotPrivateMessage": "Private",
|
"onebotPrivateMessage": "Private",
|
||||||
"onebotUserOrGroupId": "Group/User ID",
|
"onebotUserOrGroupId": "Group/User ID",
|
||||||
"onebotSafetyTips": "For safety, must set access token",
|
"onebotSafetyTips": "For safety, must set access token",
|
||||||
|
"PushDeer Server": "PushDeer Server",
|
||||||
|
"pushDeerServerDescription": "Leave blank to use the official server",
|
||||||
"PushDeer Key": "PushDeer Key",
|
"PushDeer Key": "PushDeer Key",
|
||||||
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
||||||
"Custom Monitor Type": "Custom Monitor Type",
|
"Custom Monitor Type": "Custom Monitor Type",
|
||||||
@@ -722,7 +748,8 @@
|
|||||||
"ntfyAuthenticationMethod": "Authentication Method",
|
"ntfyAuthenticationMethod": "Authentication Method",
|
||||||
"ntfyUsernameAndPassword": "Username and Password",
|
"ntfyUsernameAndPassword": "Username and Password",
|
||||||
"twilioAccountSID": "Account SID",
|
"twilioAccountSID": "Account SID",
|
||||||
"twilioAuthToken": "Auth Token",
|
"twilioApiKey": "Api Key (optional)",
|
||||||
|
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||||
"twilioFromNumber": "From Number",
|
"twilioFromNumber": "From Number",
|
||||||
"twilioToNumber": "To Number",
|
"twilioToNumber": "To Number",
|
||||||
"Monitor Setting": "{0}'s Monitor Setting",
|
"Monitor Setting": "{0}'s Monitor Setting",
|
||||||
@@ -731,13 +758,14 @@
|
|||||||
"Open Badge Generator": "Open Badge Generator",
|
"Open Badge Generator": "Open Badge Generator",
|
||||||
"Badge Generator": "{0}'s Badge Generator",
|
"Badge Generator": "{0}'s Badge Generator",
|
||||||
"Badge Type": "Badge Type",
|
"Badge Type": "Badge Type",
|
||||||
"Badge Duration": "Badge Duration",
|
"Badge Duration (in hours)": "Badge Duration (in hours)",
|
||||||
"Badge Label": "Badge Label",
|
"Badge Label": "Badge Label",
|
||||||
"Badge Prefix": "Badge Prefix",
|
"Badge Prefix": "Badge Value Prefix",
|
||||||
"Badge Suffix": "Badge Suffix",
|
"Badge Suffix": "Badge Value Suffix",
|
||||||
"Badge Label Color": "Badge Label Color",
|
"Badge Label Color": "Badge Label Color",
|
||||||
"Badge Color": "Badge Color",
|
"Badge Color": "Badge Color",
|
||||||
"Badge Label Prefix": "Badge Label Prefix",
|
"Badge Label Prefix": "Badge Label Prefix",
|
||||||
|
"Badge Preview": "Badge Preview",
|
||||||
"Badge Label Suffix": "Badge Label Suffix",
|
"Badge Label Suffix": "Badge Label Suffix",
|
||||||
"Badge Up Color": "Badge Up Color",
|
"Badge Up Color": "Badge Up Color",
|
||||||
"Badge Down Color": "Badge Down Color",
|
"Badge Down Color": "Badge Down Color",
|
||||||
@@ -751,6 +779,32 @@
|
|||||||
"Badge URL": "Badge URL",
|
"Badge URL": "Badge URL",
|
||||||
"Group": "Group",
|
"Group": "Group",
|
||||||
"Monitor Group": "Monitor Group",
|
"Monitor Group": "Monitor Group",
|
||||||
|
"Kafka Brokers": "Kafka Brokers",
|
||||||
|
"Enter the list of brokers": "Enter the list of brokers",
|
||||||
|
"Press Enter to add broker": "Press Enter to add broker",
|
||||||
|
"Kafka Topic Name": "Kafka Topic Name",
|
||||||
|
"Kafka Producer Message": "Kafka Producer Message",
|
||||||
|
"Enable Kafka SSL": "Enable Kafka SSL",
|
||||||
|
"Enable Kafka Producer Auto Topic Creation": "Enable Kafka Producer Auto Topic Creation",
|
||||||
|
"Kafka SASL Options": "Kafka SASL Options",
|
||||||
|
"Mechanism": "Mechanism",
|
||||||
|
"Pick a SASL Mechanism...": "Pick a SASL Mechanism...",
|
||||||
|
"Authorization Identity": "Authorization Identity",
|
||||||
|
"AccessKey Id": "AccessKey Id",
|
||||||
|
"Secret AccessKey": "Secret AccessKey",
|
||||||
|
"Session Token": "Session Token",
|
||||||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||||
"Close": "Close"
|
"Close": "Close",
|
||||||
|
"Request Body": "Request Body",
|
||||||
|
"wayToGetFlashDutyKey":"You can go to Channel -> (Select a Channel) -> Integrations -> Add a new integration' page, add a 'Custom Event' to get a push address, copy the Integration Key in the address. For more information, please visit",
|
||||||
|
"FlashDuty Severity":"Severity",
|
||||||
|
"nostrRelays": "Nostr relays",
|
||||||
|
"nostrRelaysHelp": "One relay URL per line",
|
||||||
|
"nostrSender": "Sender Private Key (nsec)",
|
||||||
|
"nostrRecipients": "Recipients Public Keys (npub)",
|
||||||
|
"nostrRecipientsHelp": "npub format, one per line",
|
||||||
|
"showCertificateExpiry": "Show Certificate Expiry",
|
||||||
|
"noOrBadCertificate": "No/Bad Certificate",
|
||||||
|
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||||
|
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server."
|
||||||
}
|
}
|
||||||
|
@@ -270,7 +270,7 @@
|
|||||||
"Display Timezone": "Mostrar Zona Horaria",
|
"Display Timezone": "Mostrar Zona Horaria",
|
||||||
"Server Timezone": "Servidor de Zona Horaria",
|
"Server Timezone": "Servidor de Zona Horaria",
|
||||||
"statusPageMaintenanceEndDate": "Finaliza",
|
"statusPageMaintenanceEndDate": "Finaliza",
|
||||||
"Enable DNS Cache": "Habilitar Cache DNS",
|
"Enable DNS Cache": "Habilitar Cache DNS de monitores HTTP(s)",
|
||||||
"No Maintenance": "Sin Mantenimiento",
|
"No Maintenance": "Sin Mantenimiento",
|
||||||
"weekdayShortSun": "Dom",
|
"weekdayShortSun": "Dom",
|
||||||
"dayOfWeek": "Día de la Semana",
|
"dayOfWeek": "Día de la Semana",
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
"emojiCheatSheet": "Hoja de trucos Emoji: {0}",
|
"emojiCheatSheet": "Hoja de trucos Emoji: {0}",
|
||||||
"webhookJsonDesc": "{0} es bueno para cualquier servidor HTTP moderno como Express.js",
|
"webhookJsonDesc": "{0} es bueno para cualquier servidor HTTP moderno como Express.js",
|
||||||
"webhookFormDataDesc": "{multipart} es bueno para PHP. El JSON deberá analizarse con {decodeFunction}",
|
"webhookFormDataDesc": "{multipart} es bueno para PHP. El JSON deberá analizarse con {decodeFunction}",
|
||||||
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook.",
|
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook. Cada cabecera debe definirse como una clave/valor JSON.",
|
||||||
"appriseInstalled": "Apprise está instalado.",
|
"appriseInstalled": "Apprise está instalado.",
|
||||||
"successMessage": "Mensaje de éxito",
|
"successMessage": "Mensaje de éxito",
|
||||||
"Pick Accepted Status Codes...": "Seleccione Códigos de Estado Aceptados…",
|
"Pick Accepted Status Codes...": "Seleccione Códigos de Estado Aceptados…",
|
||||||
@@ -497,8 +497,6 @@
|
|||||||
"Proto Method": "Método Proto",
|
"Proto Method": "Método Proto",
|
||||||
"Proto Content": "Contenido Proto",
|
"Proto Content": "Contenido Proto",
|
||||||
"Economy": "Económico",
|
"Economy": "Económico",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Platform": "Plataforma",
|
"Platform": "Plataforma",
|
||||||
"onebotPrivateMessage": "Privado",
|
"onebotPrivateMessage": "Privado",
|
||||||
"onebotMessageType": "Tipo de Mensaje OneBot",
|
"onebotMessageType": "Tipo de Mensaje OneBot",
|
||||||
@@ -590,8 +588,8 @@
|
|||||||
"GoogleChat": "Chat de Google (sólo Google Workspace)",
|
"GoogleChat": "Chat de Google (sólo Google Workspace)",
|
||||||
"Kook": "Kook",
|
"Kook": "Kook",
|
||||||
"wayToGetKookBotToken": "Crea aplicación y obtén tu token de bot en {0}",
|
"wayToGetKookBotToken": "Crea aplicación y obtén tu token de bot en {0}",
|
||||||
"wayToGetKookGuildID": "Activa 'Modo Desarrollador' en los ajustes de Kook, y haz click derecho en la unión para obtener su ID",
|
"wayToGetKookGuildID": "Activa 'Modo Desarrollador' en los ajustes de Kook, y haz click derecho en el grupo para obtener su ID",
|
||||||
"Guild ID": "ID de Gremio",
|
"Guild ID": "ID de grupo",
|
||||||
"User Key": "Key de Usuario",
|
"User Key": "Key de Usuario",
|
||||||
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
||||||
"octopushTypeLowCost": "Bajo Coste (Lento - algunas veces bloqueado por operador)",
|
"octopushTypeLowCost": "Bajo Coste (Lento - algunas veces bloqueado por operador)",
|
||||||
@@ -655,7 +653,7 @@
|
|||||||
"gorush": "Gorush",
|
"gorush": "Gorush",
|
||||||
"squadcast": "Squadcast",
|
"squadcast": "Squadcast",
|
||||||
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
|
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
|
||||||
"Effective Date Range": "Rango de Fecha Efectivo",
|
"Effective Date Range": "Rango de Fecha Efectivo (Opcional)",
|
||||||
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
|
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
|
||||||
"Gateway Type": "Tipo de Puerta de Enlace",
|
"Gateway Type": "Tipo de Puerta de Enlace",
|
||||||
"SMSManager": "SMSManager",
|
"SMSManager": "SMSManager",
|
||||||
@@ -698,7 +696,7 @@
|
|||||||
"High": "Alto",
|
"High": "Alto",
|
||||||
"alertaApiEndpoint": "Endpoint API",
|
"alertaApiEndpoint": "Endpoint API",
|
||||||
"Body Encoding": "Codificación del cuerpo",
|
"Body Encoding": "Codificación del cuerpo",
|
||||||
"Expiry date": "Fecha de expiración",
|
"Expiry date": "Fecha de vencimiento",
|
||||||
"Expiry": "Expiración",
|
"Expiry": "Expiración",
|
||||||
"API Keys": "Claves API",
|
"API Keys": "Claves API",
|
||||||
"Key Added": "Clave añadida",
|
"Key Added": "Clave añadida",
|
||||||
@@ -751,7 +749,22 @@
|
|||||||
"statusPageRefreshIn": "Reinicio en: {0}",
|
"statusPageRefreshIn": "Reinicio en: {0}",
|
||||||
"twilioAuthToken": "Token de Autentificación",
|
"twilioAuthToken": "Token de Autentificación",
|
||||||
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
|
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
|
||||||
"ntfyAuthenticationMethod": "Método de Autentificación",
|
"ntfyAuthenticationMethod": "Método de Autenticación",
|
||||||
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
|
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
|
||||||
"Reconnecting...": "Reconectando..."
|
"Reconnecting...": "Reconectando...",
|
||||||
|
"Select": "Seleccionar",
|
||||||
|
"chromeExecutableAutoDetect": "Auto Detectar",
|
||||||
|
"Edit Maintenance": "Editar mantenimiento",
|
||||||
|
"pushoverMessageTtl": "Mensaje TTL (segundos)",
|
||||||
|
"Notify Channel": "Canal de notificación",
|
||||||
|
"Show Clickable Link Description": "Si está marcado, todos los que tienen acceso a esta página de estado pueden tener acceso a la URL del monitor.",
|
||||||
|
"webhookBodyCustomOption": "Cuerpo Personalizado",
|
||||||
|
"selectedMonitorCount": "Seleccionado: {0}",
|
||||||
|
"Check/Uncheck": "Marcar/Desmarcar",
|
||||||
|
"Invert Keyword": "Invertir palabra clave",
|
||||||
|
"filterActive": "Activo",
|
||||||
|
"filterActivePaused": "Pausado",
|
||||||
|
"Home": "Inicio",
|
||||||
|
"Expected Value": "Valor esperado",
|
||||||
|
"Json Query": "Consulta Json"
|
||||||
}
|
}
|
||||||
|
@@ -415,8 +415,6 @@
|
|||||||
"For safety, must use secret key": "For safety, must use secret key",
|
"For safety, must use secret key": "For safety, must use secret key",
|
||||||
"Device Token": "Gailu tokena",
|
"Device Token": "Gailu tokena",
|
||||||
"Platform": "Plataforma",
|
"Platform": "Plataforma",
|
||||||
"iOS": "iOS",
|
|
||||||
"Android": "Android",
|
|
||||||
"Huawei": "Huawei",
|
"Huawei": "Huawei",
|
||||||
"High": "Altua",
|
"High": "Altua",
|
||||||
"Retry": "Errepikatu",
|
"Retry": "Errepikatu",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user