mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 22:06:59 +08:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
11d01ebc78 |
@@ -18,7 +18,6 @@ README.md
|
||||
.vscode
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.devcontainer
|
||||
/.github
|
||||
yarn.lock
|
||||
app.json
|
||||
@@ -34,13 +33,7 @@ tsconfig.json
|
||||
/ecosystem.config.js
|
||||
/extra/healthcheck.exe
|
||||
/extra/healthcheck
|
||||
/extra/exe-builder
|
||||
/extra/push-examples
|
||||
/extra/uptime-kuma-push
|
||||
|
||||
# Comment the following line if you want to rebuild the healthcheck binary
|
||||
/extra/healthcheck-armv7
|
||||
|
||||
extra/exe-builder
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
|
@@ -78,7 +78,7 @@ module.exports = {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
"space-before-blocks": "warn",
|
||||
//"no-console": "warn",
|
||||
//'no-console': 'warn',
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-multiple-empty-lines": [ "warn", {
|
||||
"max": 1,
|
||||
@@ -90,8 +90,7 @@ module.exports = {
|
||||
"no-unneeded-ternary": "error",
|
||||
"array-bracket-newline": [ "error", "consistent" ],
|
||||
"eol-last": [ "error", "always" ],
|
||||
//"prefer-template": "error",
|
||||
"template-curly-spacing": [ "warn", "never" ],
|
||||
//'prefer-template': 'error',
|
||||
"comma-dangle": [ "warn", "only-multiline" ],
|
||||
"no-empty": [ "error", {
|
||||
"allowEmptyCatch": true
|
||||
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@@ -5,11 +5,11 @@ name: Auto Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, 1.23.X ]
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
pull_request:
|
||||
branches: [ master, 1.23.X ]
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
|
||||
@@ -22,18 +22,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 14, 20 ]
|
||||
node: [ 14, 18 ]
|
||||
# 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@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install npm@9 -g
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
@@ -50,18 +50,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ARMv7 ]
|
||||
node: [ 14, 20 ]
|
||||
node: [ 14.21.3, 18.16.1 ]
|
||||
# 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@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install npm@9 -g
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm ci --production
|
||||
|
||||
check-linters:
|
||||
@@ -69,24 +69,24 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
- run: npm install
|
||||
- run: npm run lint:prod
|
||||
- run: npm run lint
|
||||
|
||||
e2e-tests:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
- run: npm install
|
||||
@@ -98,10 +98,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
- run: npm install
|
||||
|
4
.github/workflows/close-incorrect-issue.yml
vendored
4
.github/workflows/close-incorrect-issue.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
node-version: [16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
7
.github/workflows/json-yaml-validate.yml
vendored
7
.github/workflows/json-yaml-validate.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: json-yaml-validate
|
||||
name: json-yaml-validate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -6,7 +6,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 1.23.X
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -17,11 +16,11 @@ jobs:
|
||||
json-yaml-validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: json-yaml-validate
|
||||
id: json-yaml-validate
|
||||
uses: GrantBirki/json-yaml-validate@v2.4.0
|
||||
uses: GrantBirki/json-yaml-validate@v1.3.0
|
||||
with:
|
||||
comment: "true" # enable comment mode
|
||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
||||
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
|
||||
|
@@ -10,7 +10,6 @@
|
||||
"color-function-notation": "legacy",
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"color-hex-length": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"at-rule-no-unknown": null
|
||||
"declaration-block-no-redundant-longhand-properties": null
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,13 @@
|
||||
|
||||
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for the server part. Both frontend and backend share the same package.json.
|
||||
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code builds into "dist" directory. The server (express.js) exposes the "dist" directory as the root of the endpoint. This is how production is working.
|
||||
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
||||
|
||||
## Key Technical Skills
|
||||
|
||||
- Node.js (You should know about promise, async/await and arrow function etc.)
|
||||
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
||||
- Socket.io
|
||||
- SCSS
|
||||
- Vue.js
|
||||
@@ -30,24 +30,24 @@ The frontend code builds into "dist" directory. The server (express.js) exposes
|
||||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know if it will be merged or not.
|
||||
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
|
||||
|
||||
Here are some references:
|
||||
|
||||
### ✅ Usually accepted:
|
||||
✅ Usually Accept:
|
||||
- Bug fix
|
||||
- Security fix
|
||||
- Adding notification providers
|
||||
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
||||
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
|
||||
- Adding new language keys: `$t("...")`
|
||||
|
||||
### ⚠️ Discussion required:
|
||||
⚠️ Discussion First
|
||||
- Large pull requests
|
||||
- New features
|
||||
|
||||
### ❌ Won't be merged:
|
||||
- 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
|
||||
❌ Won't Merge
|
||||
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
|
||||
- Do not pass the auto test
|
||||
- Any breaking changes
|
||||
- Duplicated pull requests
|
||||
- Buggy
|
||||
@@ -61,9 +61,9 @@ The above cases may not cover all possible situations.
|
||||
|
||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||
|
||||
I will assign your pull request to a [milestone](https://github.com/louislam/uptime-kuma/milestones), if I plan to review and merge it.
|
||||
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
|
||||
|
||||
Also, please don't rush or ask for an ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
|
||||
|
||||
|
||||
### Recommended Pull Request Guideline
|
||||
@@ -83,11 +83,11 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
|
||||
|
||||
## Project Styles
|
||||
|
||||
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation will be as easy as like installing a mobile app.
|
||||
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation could be as easy as like installing a mobile app.
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, and no extra effort required to get it running
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, no extra effort required to get it running
|
||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||
- Settings should be configurable in the frontend. Environment variables are discouraged, unless it is related to startup such as `DATA_DIR`
|
||||
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`
|
||||
- Easy to use
|
||||
- The web UI styling should be consistent and nice
|
||||
|
||||
@@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
|
||||
|
||||
## Tools
|
||||
|
||||
- [`Node.js`](https://nodejs.org/) >= 14
|
||||
- [`npm`](https://www.npmjs.com/) >= 8.5
|
||||
- [`git`](https://git-scm.com/)
|
||||
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
||||
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
||||
- Node.js >= 14
|
||||
- NPM >= 8.5
|
||||
- Git
|
||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||
|
||||
## Install Dependencies for Development
|
||||
|
||||
@@ -130,7 +130,7 @@ Port `3000` and port `3001` will be used.
|
||||
npm run dev
|
||||
```
|
||||
|
||||
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
|
||||
But sometimes, you would like to keep restart the server, but not the frontend, you can run these command in two terminals:
|
||||
```
|
||||
npm run start-frontend-dev
|
||||
npm run start-server-dev
|
||||
@@ -146,13 +146,13 @@ It is mainly a socket.io app + express.js.
|
||||
express.js is used for:
|
||||
- entry point such as redirecting to a status page or the dashboard
|
||||
- serving the frontend built files (index.html, .js and .css etc.)
|
||||
- serving internal APIs of the status page
|
||||
- serving internal APIs of status page
|
||||
|
||||
|
||||
### Structure in /server/
|
||||
|
||||
- jobs/ (Jobs that are running in another process)
|
||||
- model/ (Object model, auto-mapping to the database table name)
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- monitor_types (Monitor Types)
|
||||
- notification-providers/ (individual notification logic)
|
||||
@@ -163,7 +163,7 @@ express.js is used for:
|
||||
|
||||
## Frontend Dev Server
|
||||
|
||||
It binds to `0.0.0.0:3000` by default. The frontend dev server is used for development only.
|
||||
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||
|
||||
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||
|
||||
@@ -181,7 +181,7 @@ Uptime Kuma Frontend is a single page application (SPA). Most paths are handled
|
||||
|
||||
The router is in `src/router.js`
|
||||
|
||||
As you can see, most data in the frontend is stored at the root level, even though you changed the current router to any other pages.
|
||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
||||
|
||||
The data and socket logic are in `src/mixins/socket.js`.
|
||||
|
||||
@@ -210,25 +210,15 @@ Both frontend and backend share the same package.json. However, the frontend dep
|
||||
|
||||
### Update Dependencies
|
||||
|
||||
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update the patch release version only.
|
||||
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
|
||||
|
||||
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
|
||||
|
||||
If for security / bug / other reasons, a library must be updated, breaking changes need to be checked by the person proposing the change.
|
||||
If for maybe security reasons, a library must be updated. Then you must need to check if there are any breaking changes.
|
||||
|
||||
## Translations
|
||||
|
||||
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are omitted, they can not be translated).
|
||||
|
||||
**Don't include any other languages in your initial Pull-Request** (even if this is your mother tongue), 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.
|
||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
||||
## Wiki
|
||||
|
||||
|
25
README.md
25
README.md
@@ -1,16 +1,16 @@
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
# 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>
|
||||
[](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" />
|
||||
</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="" />
|
||||
|
||||
## 🥔 Live Demo
|
||||
@@ -26,7 +26,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
||||
* 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
|
||||
* 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
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/lang)
|
||||
* Multiple status pages
|
||||
* Map status pages to specific domains
|
||||
@@ -70,7 +70,7 @@ npm run setup
|
||||
# Option 1. Try it
|
||||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in the background using PM2
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have it:
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
|
||||
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
|
||||
|
||||
### Windows Portable (x64)
|
||||
|
||||
https://github.com/louislam/uptime-kuma/releases/download/1.23.1/uptime-kuma-windows-x64-portable-1.23.1.zip
|
||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
@@ -109,7 +109,7 @@ https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
|
||||
|
||||
## 🆕 What's Next?
|
||||
|
||||
I will assign requests/issues to the next milestone.
|
||||
I will mark requests/issues to the next milestone.
|
||||
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
|
||||
@@ -184,10 +184,7 @@ If you want to report a bug or request a new feature, feel free to open a [new i
|
||||
### 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).
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
@@ -3,19 +3,19 @@
|
||||
## Reporting a Vulnerability
|
||||
|
||||
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||
1. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
|
||||
1. Please also create a empty security issues for alerting me, as GitHub Advisory do not send a notification, I probably will miss without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
|
||||
|
||||
Do not use the public issue tracker or discuss it in public as it will cause more damage.
|
||||
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
|
||||
|
||||
## Do you accept other 3rd-party bug bounty platforms?
|
||||
|
||||
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone has tried to send a phishing link to me by doing this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
|
||||
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone have tried to send a phishing link to me by this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the latest version.
|
||||
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
|
@@ -4,4 +4,8 @@ if (process.env.TEST_FRONTEND) {
|
||||
config.presets = [ "@babel/preset-env" ];
|
||||
}
|
||||
|
||||
if (process.env.TEST_BACKEND) {
|
||||
config.plugins = [ "babel-plugin-rewire" ];
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
import visualizer from "rollup-plugin-visualizer";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
@@ -21,6 +22,7 @@ export default defineConfig({
|
||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||
},
|
||||
plugins: [
|
||||
commonjs(),
|
||||
vue(),
|
||||
legacy({
|
||||
targets: [ "since 2015" ],
|
||||
|
@@ -1,7 +0,0 @@
|
||||
-- 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;
|
@@ -1,7 +0,0 @@
|
||||
-- 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;
|
@@ -1,6 +0,0 @@
|
||||
-- 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;
|
@@ -1,34 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Rename COLUMNs to another one (suffixed by `_old`)
|
||||
ALTER TABLE monitor
|
||||
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
|
||||
|
||||
ALTER TABLE monitor
|
||||
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
|
||||
|
||||
-- Add correct COLUMNs
|
||||
ALTER TABLE monitor
|
||||
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
|
||||
|
||||
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
|
||||
|
||||
-- Set bring old values from `_old` COLUMNs to correct ones
|
||||
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
|
||||
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
|
||||
|
||||
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
|
||||
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
|
||||
|
||||
-- Remove old COLUMNs
|
||||
ALTER TABLE monitor
|
||||
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
|
||||
|
||||
ALTER TABLE monitor
|
||||
DROP COLUMN kafka_producer_ssl_old;
|
||||
|
||||
COMMIT;
|
@@ -1,19 +0,0 @@
|
||||
-- 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;
|
@@ -1,10 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- SQLite: Change the data type of the column "config" from VARCHAR to TEXT
|
||||
ALTER TABLE notification RENAME COLUMN config TO config_old;
|
||||
ALTER TABLE notification ADD COLUMN config TEXT;
|
||||
UPDATE notification SET config = config_old;
|
||||
ALTER TABLE notification DROP COLUMN config_old;
|
||||
|
||||
COMMIT;
|
@@ -1,7 +0,0 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
UPDATE monitor SET timeout = (interval * 0.8)
|
||||
WHERE timeout IS NULL OR timeout <= 0;
|
||||
|
||||
COMMIT;
|
@@ -1,33 +1,17 @@
|
||||
# DON'T UPDATE TO bullseye-slim, see #372.
|
||||
# There is no 20-buster-slim for armv7 unfortunately, 18-buster-slim is the last one for Uptime Kuma v1.
|
||||
FROM node:18-buster-slim
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:16-buster-slim
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
|
||||
# python3* = apprise's dependencies
|
||||
# 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
|
||||
# Install Curl
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
# 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!
|
||||
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 \
|
||||
sqlite3 \
|
||||
iputils-ping \
|
||||
util-linux \
|
||||
dumb-init \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nscd && \
|
||||
pip3 --no-cache-dir install apprise==1.6.0 && \
|
||||
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 \
|
||||
sqlite3 iputils-ping util-linux dumb-init git curl ca-certificates && \
|
||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
@@ -42,7 +26,3 @@ RUN set -eux && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
# For nscd
|
||||
COPY ./docker/etc/nscd.conf /etc/nscd.conf
|
||||
COPY ./docker/etc/sudoers /etc/sudoers
|
||||
|
||||
|
@@ -1,90 +0,0 @@
|
||||
#
|
||||
# /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
|
@@ -1,31 +0,0 @@
|
||||
#
|
||||
# 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
|
@@ -6,7 +6,7 @@
|
||||
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||
*/
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
const { FBSD } = require("../server/util-server");
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "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
|
||||
println("=====================");
|
||||
println("Uptime Kuma Install Script");
|
||||
println("Uptime Kuma Installer");
|
||||
println("=====================");
|
||||
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
|
||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
||||
println("---------------------------------------");
|
||||
println("This script is designed for Linux and basic usage.");
|
||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||
println("---------------------------------------");
|
||||
println("");
|
||||
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
|
||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
||||
println("Docker - Install Uptime Kuma Docker container");
|
||||
println("");
|
||||
|
||||
@@ -29,10 +29,14 @@ function checkNode() {
|
||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||
println("Node Version: " ++ nodeVersion);
|
||||
|
||||
if (nodeVersion <= "12") {
|
||||
if (nodeVersion < "12") {
|
||||
println("Error: Required Node.js 14");
|
||||
call("exit", "1");
|
||||
}
|
||||
|
||||
if (nodeVersion == "12") {
|
||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
||||
}
|
||||
}
|
||||
|
||||
function deb() {
|
||||
@@ -56,8 +60,8 @@ function deb() {
|
||||
bash("apt --yes install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 16");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("apt --yes install nodejs");
|
||||
bash("node -v");
|
||||
|
||||
@@ -87,10 +91,6 @@ if (type == "local") {
|
||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||
if (os == "Ubuntu") {
|
||||
distribution = "ubuntu";
|
||||
|
||||
// Get ubuntu version
|
||||
bash(". /etc/lsb-release");
|
||||
version = DISTRIB_RELEASE;
|
||||
}
|
||||
if (os == "Debian") {
|
||||
distribution = "debian";
|
||||
@@ -101,7 +101,6 @@ if (type == "local") {
|
||||
|
||||
println("Your OS: " ++ os);
|
||||
println("Distribution: " ++ distribution);
|
||||
println("Version: " ++ version);
|
||||
println("Arch: " ++ arch);
|
||||
|
||||
if ("$3" != "") {
|
||||
@@ -132,32 +131,15 @@ if (type == "local") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
bash("dnfCheck=$(dnf --version)");
|
||||
|
||||
// Use yum
|
||||
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");
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("yum -y -q install curl");
|
||||
}
|
||||
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("yum install -y -q nodejs");
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
@@ -189,15 +171,13 @@ if (type == "local") {
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: git is not found!");
|
||||
println("help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git");
|
||||
println("Error: git is missing");
|
||||
}
|
||||
|
||||
bash("check=$(node -v)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: node is not found");
|
||||
println("help: an installation guide is available at https://nodejs.org/en/download");
|
||||
println("Error: node is missing");
|
||||
}
|
||||
|
||||
if (error > 0) {
|
||||
@@ -213,15 +193,6 @@ if (type == "local") {
|
||||
bash("pm2 startup");
|
||||
}
|
||||
|
||||
|
||||
// Check again
|
||||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Error: pm2 is not found!");
|
||||
println("help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("mkdir -p $installPath");
|
||||
bash("cd $installPath");
|
||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||
@@ -235,7 +206,6 @@ if (type == "local") {
|
||||
bash("check=$(docker -v)");
|
||||
if (check == "") {
|
||||
println("Error: docker is not found!");
|
||||
println("help: an installation guide is available at https://docs.docker.com/desktop/");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
@@ -243,7 +213,6 @@ if (type == "local") {
|
||||
|
||||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
|
||||
\"echo\" \"Error: docker is not running\"
|
||||
\"echo\" \"help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/\"
|
||||
\"exit\" \"1\"
|
||||
fi");
|
||||
|
||||
|
@@ -1,44 +0,0 @@
|
||||
// Generate on GitHub
|
||||
const input = `
|
||||
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||
`;
|
||||
|
||||
const template = `
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
|
||||
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||
|
||||
for (const line of lines) {
|
||||
// Split the last " by "
|
||||
const usernamePullRequesURL = line.split(" by ").pop();
|
||||
|
||||
if (!usernamePullRequesURL) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||
let message = line.split(" by ").shift();
|
||||
|
||||
if (!message) {
|
||||
console.log("Unable to parse", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
message = message.split("* ").pop();
|
||||
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||
}
|
||||
console.log(template);
|
@@ -5,8 +5,6 @@ const { R } = require("redbean-node");
|
||||
const readline = require("readline");
|
||||
const { initJWTSecret } = require("../server/util-server");
|
||||
const User = require("../server/model/user");
|
||||
const { io } = require("socket.io-client");
|
||||
const { localWebSocketURL } = require("../server/config");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@@ -38,16 +36,12 @@ const main = async () => {
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
// Disconnect all other socket clients of the user
|
||||
await disconnectAllSocketClients(user.username, password);
|
||||
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
}
|
||||
}
|
||||
console.log("Password reset successfully.");
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error: " + e.message);
|
||||
@@ -72,44 +66,6 @@ function question(question) {
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectAllSocketClients(username, password) {
|
||||
return new Promise((resolve) => {
|
||||
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
|
||||
|
||||
// Disconnect all socket connections
|
||||
const socket = io(localWebSocketURL, {
|
||||
reconnection: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
socket.emit("login", {
|
||||
username,
|
||||
password,
|
||||
}, (res) => {
|
||||
if (res.ok) {
|
||||
console.log("Logged in.");
|
||||
socket.emit("disconnectOtherSocketClients");
|
||||
} else {
|
||||
console.warn("Login failed.");
|
||||
console.warn("Please restart the server to disconnect all sessions.");
|
||||
}
|
||||
socket.close();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("connect_error", function () {
|
||||
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
|
||||
// Ask the user to restart the server manually
|
||||
console.warn("Failed to connect to " + localWebSocketURL);
|
||||
console.warn("Please restart the server to disconnect all sessions manually.");
|
||||
resolve();
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
main();
|
||||
}
|
||||
|
16
index.html
16
index.html
@@ -9,24 +9,8 @@
|
||||
<meta name="theme-color" id="theme-color" content="" />
|
||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
<title>Uptime Kuma</title>
|
||||
<style>
|
||||
.noscript-message {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="noscript-message">
|
||||
Sorry, you don't seem to have JavaScript enabled or your browser
|
||||
doesn't support it.<br />This website requires JavaScript to function.
|
||||
Please enable JavaScript in your browser settings to continue.
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
63
install.sh
63
install.sh
@@ -3,15 +3,15 @@
|
||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Uptime Kuma Install Script"
|
||||
"echo" "-e" "Uptime Kuma Installer"
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
|
||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" ""
|
||||
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
|
||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||
"echo" "-e" ""
|
||||
if [ "$1" != "" ]; then
|
||||
@@ -25,9 +25,12 @@ function checkNode {
|
||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||
_0="12"
|
||||
if [ $(($nodeVersion <= $_0)) == 1 ]; then
|
||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
||||
"echo" "-e" "Error: Required Node.js 14"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$nodeVersion" == "12" ]; then
|
||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
||||
fi
|
||||
}
|
||||
function deb {
|
||||
@@ -47,8 +50,8 @@ fi
|
||||
"echo" "-e" "Installing Curl"
|
||||
apt --yes install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 16"
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
||||
apt --yes install nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
@@ -72,10 +75,7 @@ if [ "$type" == "local" ]; then
|
||||
if [ -e "/etc/issue" ]; then
|
||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||
if [ "$os" == "Ubuntu" ]; then
|
||||
distribution="ubuntu"
|
||||
# Get ubuntu version
|
||||
. /etc/lsb-release
|
||||
version="$DISTRIB_RELEASE"
|
||||
distribution="ubuntu"
|
||||
fi
|
||||
if [ "$os" == "Debian" ]; then
|
||||
distribution="debian"
|
||||
@@ -85,7 +85,6 @@ fi
|
||||
arch=$(uname -i)
|
||||
"echo" "-e" "Your OS: ""$os"
|
||||
"echo" "-e" "Distribution: ""$distribution"
|
||||
"echo" "-e" "Version: ""$version"
|
||||
"echo" "-e" "Arch: ""$arch"
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
@@ -109,27 +108,14 @@ fi
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
dnfCheck=$(dnf --version)
|
||||
# Use yum
|
||||
if [ "$dnfCheck" == "" ]; then
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 16"
|
||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
||||
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
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
||||
yum install -y -q nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
@@ -156,14 +142,12 @@ fi
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: git is not found!"
|
||||
"echo" "-e" "help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
|
||||
"echo" "-e" "Error: git is missing"
|
||||
fi
|
||||
check=$(node -v)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: node is not found"
|
||||
"echo" "-e" "help: an installation guide is available at https://nodejs.org/en/download"
|
||||
"echo" "-e" "Error: node is missing"
|
||||
fi
|
||||
if [ $(($error > 0)) == 1 ]; then
|
||||
"echo" "-e" "Please install above missing software"
|
||||
@@ -177,13 +161,6 @@ fi
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
pm2 startup
|
||||
fi
|
||||
# Check again
|
||||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Error: pm2 is not found!"
|
||||
"echo" "-e" "help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
cd $installPath
|
||||
@@ -195,13 +172,11 @@ else
|
||||
check=$(docker -v)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Error: docker is not found!"
|
||||
"echo" "-e" "help: an installation guide is available at https://docs.docker.com/desktop/"
|
||||
exit 1
|
||||
fi
|
||||
check=$(docker info)
|
||||
if [[ "$check" == *"Is the docker daemon running"* ]]; then
|
||||
"echo" "Error: docker is not running"
|
||||
"echo" "help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$3" != "" ]; then
|
||||
|
5240
package-lock.json
generated
5240
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.23.11",
|
||||
"version": "1.22.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -13,12 +13,10 @@
|
||||
"install-legacy": "npm install",
|
||||
"update-legacy": "npm update",
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:js-prod": "npm run lint:js -- --max-warnings 0",
|
||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"lint:prod": "npm run lint:js-prod && npm run lint:style",
|
||||
"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-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
||||
@@ -42,25 +40,19 @@
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.23.11 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"remove-2fa": "node extra/remove-2fa.js",
|
||||
"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-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-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-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
|
||||
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
|
||||
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||
"ncu-patch": "npm-check-updates -u -t patch",
|
||||
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
@@ -74,8 +66,7 @@
|
||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
||||
"sort-contributors": "node extra/sort-contributors.js",
|
||||
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
|
||||
"sort-contributors": "node extra/sort-contributors.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
@@ -101,13 +92,11 @@
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"form-data": "~4.0.0",
|
||||
"gamedig": "^4.2.0",
|
||||
"html-escaper": "^3.0.3",
|
||||
"gamedig": "~4.0.5",
|
||||
"http-graceful-shutdown": "~3.1.7",
|
||||
"http-proxy-agent": "~5.0.0",
|
||||
"https-proxy-agent": "~5.0.1",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
@@ -115,26 +104,24 @@
|
||||
"kafkajs": "^2.2.4",
|
||||
"limiter": "~2.1.0",
|
||||
"liquidjs": "^10.7.0",
|
||||
"mongodb": "~4.17.1",
|
||||
"mongodb": "~4.14.0",
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
"mysql2": "~3.6.2",
|
||||
"mysql2": "~2.3.3",
|
||||
"nanoid": "~3.3.4",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-client": "~1.0.0",
|
||||
"nodemailer": "~6.6.5",
|
||||
"nostr-tools": "^1.13.1",
|
||||
"notp": "~2.0.3",
|
||||
"openid-client": "^5.4.2",
|
||||
"password-hash": "~1.2.2",
|
||||
"pg": "~8.11.3",
|
||||
"pg-connection-string": "~2.6.2",
|
||||
"pg": "~8.8.0",
|
||||
"pg-connection-string": "~2.5.0",
|
||||
"playwright-core": "~1.35.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"promisify-child-process": "~4.1.2",
|
||||
"protobufjs": "~7.2.4",
|
||||
"qs": "~6.10.4",
|
||||
"queue": "~7.0.0",
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"semver": "~7.5.4",
|
||||
@@ -143,8 +130,7 @@
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"tar": "~6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"ws": "^8.13.0"
|
||||
"thirty-two": "~1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
@@ -161,6 +147,7 @@
|
||||
"@vue/compiler-sfc": "~3.3.4",
|
||||
"@vuepic/vue-datepicker": "~3.4.8",
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"chart.js": "~4.2.1",
|
||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
||||
@@ -168,7 +155,7 @@
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"cypress": "^13.2.0",
|
||||
"cypress": "^12.17.0",
|
||||
"delay": "^5.0.0",
|
||||
"dns2": "~2.0.1",
|
||||
"dompurify": "~2.4.3",
|
||||
@@ -192,6 +179,7 @@
|
||||
"typescript": "~4.4.4",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~4.4.1",
|
||||
"vite-plugin-commonjs": "^0.8.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "~3.3.4",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
@@ -205,7 +193,7 @@
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"wait-on": "^6.0.1",
|
||||
"whatwg-url": "~12.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
<svg width="640" height="640" viewBox="0 0 640 640" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1 0 0 1 320 320)">
|
||||
<linearGradient id="S3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 -319.99875 -320.0001577393)" x1="259.78" y1="261.15" x2="463.85" y2="456.49">
|
||||
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" fill="url(#paint0_linear_381_799)"/>
|
||||
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" stroke="#F2F2F2" stroke-opacity="0.51" stroke-width="200"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_381_799" x1="259.78" y1="261.15" x2="463.85" y2="456.49" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5CDD8B"/>
|
||||
<stop offset="1" stop-color="#86E6A9"/>
|
||||
</linearGradient>
|
||||
<path style="stroke: rgb(242,242,242); stroke-opacity: 0.51; stroke-width: 200; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#S3); fill-rule: nonzero; opacity: 1;" transform=" translate(0, 0)" d="M 170.40125 -84.36016 C 224.09125 38.37984 224.09125 115.33984 170.40125 146.49984 C 89.85125000000001 193.23984000000002 -120.03875 207.48984000000002 -180.45875 135.63984 C -220.73875 87.73983999999999 -220.73875 14.399839999999998 -180.45875 -84.36016000000001 C -139.49875 -151.82016 -81.28875000000001 -185.55016 -5.828750000000014 -185.55016 C 69.64124999999999 -185.55016 128.38125 -151.82016000000002 170.40124999999998 -84.36016000000001 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 893 B |
@@ -147,18 +147,15 @@ async function sendAPIKeyList(socket) {
|
||||
async function sendInfo(socket, hideVersion = false) {
|
||||
let version;
|
||||
let latestVersion;
|
||||
let isContainer;
|
||||
|
||||
if (!hideVersion) {
|
||||
version = checkVersion.version;
|
||||
latestVersion = checkVersion.latestVersion;
|
||||
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
|
||||
}
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
latestVersion,
|
||||
isContainer,
|
||||
primaryBaseURL: await setting("primaryBaseURL"),
|
||||
serverTimezone: await server.getTimezone(),
|
||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||
|
@@ -1,42 +1,29 @@
|
||||
const isFreeBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
// Interop with browser
|
||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = isFreeBSD ? null : process.env.HOST;
|
||||
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||
|
||||
const isSSL = sslKey && sslCert;
|
||||
|
||||
function getLocalWebSocketURL() {
|
||||
const protocol = isSSL ? "wss" : "ws";
|
||||
const host = hostname || "localhost";
|
||||
return `${protocol}://${host}:${port}`;
|
||||
}
|
||||
|
||||
const localWebSocketURL = getLocalWebSocketURL();
|
||||
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
naColor: "#999",
|
||||
defaultUpColor: "#66c20a",
|
||||
defaultWarnColor: "#eed202",
|
||||
defaultDownColor: "#c2290a",
|
||||
defaultPendingColor: "#f8a306",
|
||||
defaultMaintenanceColor: "#1747f5",
|
||||
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||
defaultStyle: "flat",
|
||||
defaultPingValueSuffix: "ms",
|
||||
defaultPingLabelSuffix: "h",
|
||||
defaultUptimeValueSuffix: "%",
|
||||
defaultUptimeLabelSuffix: "h",
|
||||
defaultCertExpValueSuffix: " days",
|
||||
defaultCertExpLabelSuffix: "h",
|
||||
// Values Come From Default Notification Times
|
||||
defaultCertExpireWarnDays: "14",
|
||||
defaultCertExpireDownDays: "7"
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
hostname,
|
||||
port,
|
||||
sslKey,
|
||||
sslCert,
|
||||
sslKeyPassphrase,
|
||||
isSSL,
|
||||
localWebSocketURL,
|
||||
demoMode,
|
||||
badgeConstants,
|
||||
};
|
||||
|
@@ -3,7 +3,6 @@ const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const knex = require("knex");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@@ -26,8 +25,6 @@ class Database {
|
||||
|
||||
static path;
|
||||
|
||||
static dockerTLSDir;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@@ -77,13 +74,6 @@ class Database {
|
||||
"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,
|
||||
"patch-notification-config.sql": true,
|
||||
"patch-fix-kafka-producer-booleans.sql": true,
|
||||
"patch-timeout.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,28 +92,23 @@ class Database {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
|
||||
Database.path = path.join(Database.dataDir, "kuma.db");
|
||||
Database.path = Database.dataDir + "kuma.db";
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
Database.uploadDir = path.join(Database.dataDir, "upload/");
|
||||
Database.uploadDir = Database.dataDir + "upload/";
|
||||
|
||||
if (! fs.existsSync(Database.uploadDir)) {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create screenshot dir
|
||||
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
|
||||
Database.screenshotDir = 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}`);
|
||||
}
|
||||
|
||||
|
@@ -2,16 +2,8 @@ const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const version = require("../package.json").version;
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Database = require("./database");
|
||||
|
||||
class DockerHost {
|
||||
|
||||
static CertificateFileNameCA = "ca.pem";
|
||||
static CertificateFileNameCert = "cert.pem";
|
||||
static CertificateFileNameKey = "key.pem";
|
||||
|
||||
/**
|
||||
* Save a docker host
|
||||
* @param {Object} dockerHost Docker host to save
|
||||
@@ -74,13 +66,16 @@ class DockerHost {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
};
|
||||
|
||||
if (dockerHost.dockerType === "socket") {
|
||||
options.socketPath = dockerHost.dockerDaemon;
|
||||
} else if (dockerHost.dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||
}
|
||||
|
||||
let res = await axios.request(options);
|
||||
@@ -116,53 +111,6 @@ class DockerHost {
|
||||
}
|
||||
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 = {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
const jsesc = require("jsesc");
|
||||
const { escape } = require("html-escaper");
|
||||
|
||||
/**
|
||||
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
||||
@@ -8,18 +7,15 @@ const { escape } = require("html-escaper");
|
||||
* @returns {string}
|
||||
*/
|
||||
function getGoogleAnalyticsScript(tagId) {
|
||||
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
|
||||
let escapedTagId = jsesc(tagId, { isScriptContext: true });
|
||||
|
||||
if (escapedTagIdJS) {
|
||||
escapedTagIdJS = escapedTagIdJS.trim();
|
||||
if (escapedTagId) {
|
||||
escapedTagId = escapedTagId.trim();
|
||||
}
|
||||
|
||||
// Escape the tag ID for use in an HTML attribute.
|
||||
let escapedTagIdHTMLAttribute = escape(tagId);
|
||||
|
||||
return `
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
|
||||
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
|
||||
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -9,12 +9,12 @@ class Group extends BeanModel {
|
||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
|
||||
monitorList.push(await bean.toPublicJSON(showTags));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@@ -22,9 +22,6 @@ const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
const jsonata = require("jsonata");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const rootCertificates = rootCertificatesFingerprints();
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -40,12 +37,11 @@ class Monitor extends BeanModel {
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sendUrl: this.sendUrl,
|
||||
type: this.type,
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
@@ -55,13 +51,6 @@ class Monitor extends BeanModel {
|
||||
if (showTags) {
|
||||
obj.tags = await this.getTags();
|
||||
}
|
||||
|
||||
if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
|
||||
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
||||
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
||||
obj.validCert = validCert;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -105,7 +94,6 @@ class Monitor extends BeanModel {
|
||||
active: await this.isActive(),
|
||||
forceInactive: !await Monitor.isParentActive(this.id),
|
||||
type: this.type,
|
||||
timeout: this.timeout,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
resendInterval: this.resendInterval,
|
||||
@@ -138,14 +126,13 @@ class Monitor extends BeanModel {
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
|
||||
httpBodyEncoding: this.httpBodyEncoding,
|
||||
jsonPath: this.jsonPath,
|
||||
expectedValue: this.expectedValue,
|
||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||
kafkaProducerSsl: this.getKafkaProducerSsl(),
|
||||
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
|
||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||
screenshot,
|
||||
};
|
||||
@@ -159,11 +146,6 @@ class Monitor extends BeanModel {
|
||||
grpcMetadata: this.grpcMetadata,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
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,
|
||||
databaseConnectionString: this.databaseConnectionString,
|
||||
radiusUsername: this.radiusUsername,
|
||||
@@ -202,31 +184,6 @@ 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 ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
@@ -284,26 +241,6 @@ class Monitor extends BeanModel {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
getGameDigGivenPortOnly() {
|
||||
return Boolean(this.gamedigGivenPortOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean} Kafka Producer Ssl enabled?
|
||||
*/
|
||||
getKafkaProducerSsl() {
|
||||
return Boolean(this.kafkaProducerSsl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
|
||||
*/
|
||||
getKafkaProducerAllowAutoTopicCreation() {
|
||||
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
@@ -358,12 +295,6 @@ class Monitor extends BeanModel {
|
||||
bean.duration = 0;
|
||||
}
|
||||
|
||||
// Runtime patch timeout if it is 0
|
||||
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
|
||||
if (!this.timeout || this.timeout <= 0) {
|
||||
this.timeout = this.interval * 1000 * 0.8;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||
bean.msg = "Monitor under maintenance";
|
||||
@@ -382,10 +313,7 @@ class Monitor extends BeanModel {
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
// Only change state if the monitor is in worse conditions then the ones before
|
||||
// lastBeat.status could be null
|
||||
if (!lastBeat) {
|
||||
bean.status = PENDING;
|
||||
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
bean.status = lastBeat.status;
|
||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||
bean.status = lastBeat.status;
|
||||
@@ -413,28 +341,9 @@ 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 = {
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||
};
|
||||
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
@@ -460,20 +369,18 @@ class Monitor extends BeanModel {
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
timeout: this.timeout * 1000,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
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",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(oauth2AuthHeader),
|
||||
...(this.headers ? JSON.parse(this.headers) : {})
|
||||
},
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
signal: axiosAbortSignal((this.timeout + 10) * 1000),
|
||||
};
|
||||
|
||||
if (bodyValue) {
|
||||
@@ -608,13 +515,13 @@ class Monitor extends BeanModel {
|
||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT" || this.dns_resolve_type === "PTR") {
|
||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
|
||||
dnsMessage += "Records: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type === "CNAME") {
|
||||
dnsMessage += dnsRes[0];
|
||||
} else if (this.dns_resolve_type === "CNAME" || this.dns_resolve_type === "PTR") {
|
||||
dnsMessage = dnsRes[0];
|
||||
} else if (this.dns_resolve_type === "CAA") {
|
||||
dnsMessage += dnsRes[0].issue;
|
||||
dnsMessage = dnsRes[0].issue;
|
||||
} else if (this.dns_resolve_type === "MX") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
@@ -632,7 +539,7 @@ class Monitor extends BeanModel {
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
}
|
||||
|
||||
if (this.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
|
||||
if (this.dnsLastResult !== dnsMessage) {
|
||||
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
|
||||
dnsMessage,
|
||||
this.id
|
||||
@@ -681,7 +588,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
let res = await axios.get(steamApiUrl, {
|
||||
timeout: this.timeout * 1000,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
@@ -689,7 +596,6 @@ class Monitor extends BeanModel {
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
@@ -720,7 +626,7 @@ class Monitor extends BeanModel {
|
||||
type: this.game,
|
||||
host: this.hostname,
|
||||
port: this.port,
|
||||
givenPortOnly: this.getGameDigGivenPortOnly(),
|
||||
givenPortOnly: true,
|
||||
});
|
||||
|
||||
bean.msg = state.name;
|
||||
@@ -732,6 +638,8 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "docker") {
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
||||
|
||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||
|
||||
const options = {
|
||||
url: `/containers/${this.docker_container}/json`,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
@@ -742,26 +650,16 @@ class Monitor extends BeanModel {
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||
|
||||
if (!dockerHost) {
|
||||
throw new Error("Failed to load docker host config");
|
||||
}
|
||||
|
||||
if (dockerHost._dockerType === "socket") {
|
||||
options.socketPath = dockerHost._dockerDaemon;
|
||||
} else if (dockerHost._dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
|
||||
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
@@ -789,7 +687,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "sqlserver") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
@@ -828,7 +726,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "postgres") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
@@ -836,11 +734,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
// Use `radius_password` as `password` field, since there are too many unnecessary fields
|
||||
// TODO: rename `radius_password` to `password` later for general use
|
||||
let mysqlPassword = this.radiusPassword;
|
||||
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
@@ -865,19 +759,29 @@ class Monitor extends BeanModel {
|
||||
port = this.port;
|
||||
}
|
||||
|
||||
const resp = await radius(
|
||||
this.hostname,
|
||||
this.radiusUsername,
|
||||
this.radiusPassword,
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret,
|
||||
port,
|
||||
this.interval * 1000 * 0.4,
|
||||
);
|
||||
|
||||
bean.msg = resp.code;
|
||||
bean.status = UP;
|
||||
try {
|
||||
const resp = await radius(
|
||||
this.hostname,
|
||||
this.radiusUsername,
|
||||
this.radiusPassword,
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret,
|
||||
port,
|
||||
this.interval * 1000 * 0.8,
|
||||
);
|
||||
if (resp.code) {
|
||||
bean.msg = resp.code;
|
||||
}
|
||||
bean.status = UP;
|
||||
} catch (error) {
|
||||
bean.status = DOWN;
|
||||
if (error.response?.code) {
|
||||
bean.msg = error.response.code;
|
||||
} else {
|
||||
bean.msg = error.message;
|
||||
}
|
||||
}
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
@@ -928,11 +832,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if (error?.name === "CanceledError") {
|
||||
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||
} else {
|
||||
bean.msg = error.message;
|
||||
}
|
||||
bean.msg = error.message;
|
||||
|
||||
// If UP come in here, it must be upside down mode
|
||||
// Just reset the retries
|
||||
@@ -1116,19 +1016,6 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: http: or https:
|
||||
* @returns {(null|string)}
|
||||
*/
|
||||
getURLProtocol() {
|
||||
const url = this.getUrl();
|
||||
if (url) {
|
||||
return this.getUrl().protocol;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
@@ -1465,10 +1352,7 @@ class Monitor extends BeanModel {
|
||||
let certInfo = tlsInfoObject.certInfo;
|
||||
while (certInfo) {
|
||||
let subjectCN = certInfo.subject["CN"];
|
||||
if (rootCertificates.has(certInfo.fingerprint256)) {
|
||||
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||
break;
|
||||
} else if (certInfo.daysRemaining > targetDays) {
|
||||
if (certInfo.daysRemaining > targetDays) {
|
||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||
} else {
|
||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
||||
|
@@ -90,8 +90,6 @@ class StatusPage extends BeanModel {
|
||||
* @param {StatusPage} statusPage
|
||||
*/
|
||||
static async getStatusPageData(statusPage) {
|
||||
const config = await statusPage.toPublicJSON();
|
||||
|
||||
// Incident
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||
statusPage.id,
|
||||
@@ -112,13 +110,13 @@ class StatusPage extends BeanModel {
|
||||
]);
|
||||
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
// Response
|
||||
return {
|
||||
config,
|
||||
config: await statusPage.toPublicJSON(),
|
||||
incident,
|
||||
publicGroupList,
|
||||
maintenanceList,
|
||||
@@ -236,7 +234,6 @@ class StatusPage extends BeanModel {
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -258,7 +255,6 @@ class StatusPage extends BeanModel {
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const passwordHash = require("../password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { shake256, SHAKE256_LENGTH } = require("../util-server");
|
||||
|
||||
class User extends BeanModel {
|
||||
/**
|
||||
@@ -29,19 +27,6 @@ class User extends BeanModel {
|
||||
this.password = newPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new JWT for a user
|
||||
* @param {User} user
|
||||
* @param {string} jwtSecret
|
||||
* @return {string}
|
||||
*/
|
||||
static createJWT(user, jwtSecret) {
|
||||
return jwt.sign({
|
||||
username: user.username,
|
||||
h: shake256(user.password, SHAKE256_LENGTH),
|
||||
}, jwtSecret);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
|
@@ -40,7 +40,6 @@ if (process.platform === "win32") {
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/google-chrome",
|
||||
"/snap/bin/chromium", // Ubuntu
|
||||
];
|
||||
} else if (process.platform === "darwin") {
|
||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||
|
@@ -1,88 +0,0 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// 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.
|
||||
* @param {number} interval
|
||||
* @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 timeout = interval * 1000 * 0.8;
|
||||
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||
timeout: timeout,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (res.stderr && res.stderr.toString()) {
|
||||
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||
}
|
||||
if (res.stdout && res.stdout.toString()) {
|
||||
return res.stdout.toString();
|
||||
} else {
|
||||
throw new Error("No output from Tailscale ping");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = "OK";
|
||||
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,
|
||||
};
|
@@ -18,7 +18,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
status: this.statusToString(heartbeatJSON["status"]),
|
||||
msg: heartbeatJSON["msg"],
|
||||
});
|
||||
if (await this.sendSms(notification, msgBody)) {
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
@@ -28,7 +28,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
status: "",
|
||||
msg: msg,
|
||||
});
|
||||
if (await this.sendSms(notification, msgBody)) {
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
if (result.data.Message === "OK") {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error(result.data.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class Apprise extends NotificationProvider {
|
||||
|
||||
@@ -11,9 +11,7 @@ class Apprise extends NotificationProvider {
|
||||
args.push("-t");
|
||||
args.push(notification.title);
|
||||
}
|
||||
const s = await childProcessAsync.spawn("apprise", args, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const s = childProcess.spawnSync("apprise", args);
|
||||
|
||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
|
@@ -30,7 +30,6 @@ class Discord extends NotificationProvider {
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "gamedig":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
|
@@ -1,98 +0,0 @@
|
||||
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;
|
@@ -1,119 +0,0 @@
|
||||
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;
|
@@ -20,10 +20,10 @@ class Opsgenie extends NotificationProvider {
|
||||
|
||||
try {
|
||||
switch (notification.opsgenieRegion) {
|
||||
case "us":
|
||||
case "US":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
||||
break;
|
||||
case "eu":
|
||||
case "EU":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
|
||||
break;
|
||||
default:
|
||||
|
@@ -8,9 +8,7 @@ class PushDeer extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let endpoint = "/message/push";
|
||||
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
|
||||
let pushdeerlink = "https://api2.pushdeer.com/message/push";
|
||||
|
||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||
|
||||
|
@@ -13,7 +13,7 @@ class SMTP extends NotificationProvider {
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
tls: {
|
||||
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -21,13 +21,11 @@ const LineNotify = require("./notification-providers/linenotify");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
const Nostr = require("./notification-providers/nostr");
|
||||
const Ntfy = require("./notification-providers/ntfy");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const OneBot = require("./notification-providers/onebot");
|
||||
const Opsgenie = require("./notification-providers/opsgenie");
|
||||
const PagerDuty = require("./notification-providers/pagerduty");
|
||||
const FlashDuty = require("./notification-providers/flashduty");
|
||||
const PagerTree = require("./notification-providers/pagertree");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
@@ -86,13 +84,11 @@ class Notification {
|
||||
new LunaSea(),
|
||||
new Matrix(),
|
||||
new Mattermost(),
|
||||
new Nostr(),
|
||||
new Ntfy(),
|
||||
new Octopush(),
|
||||
new OneBot(),
|
||||
new Opsgenie(),
|
||||
new PagerDuty(),
|
||||
new FlashDuty(),
|
||||
new PagerTree(),
|
||||
new PromoSMS(),
|
||||
new Pushbullet(),
|
||||
@@ -119,6 +115,7 @@ class Notification {
|
||||
new GoAlert(),
|
||||
new ZohoCliq()
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
if (! item.name) {
|
||||
throw new Error("Notification provider without name");
|
||||
|
@@ -1,21 +1,15 @@
|
||||
let express = require("express");
|
||||
const {
|
||||
setting,
|
||||
allowDevAllOrigin,
|
||||
allowAllOrigin,
|
||||
percentageToColor,
|
||||
filterAndJoin,
|
||||
sendHttpError,
|
||||
} = require("../util-server");
|
||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
|
||||
let router = express.Router();
|
||||
@@ -28,14 +22,10 @@ router.get("/api/entry-page", async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
let result = { };
|
||||
let hostname = request.hostname;
|
||||
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
|
||||
hostname = request.headers["x-forwarded-host"];
|
||||
}
|
||||
|
||||
if (hostname in StatusPage.domainMappingList) {
|
||||
if (request.hostname in StatusPage.domainMappingList) {
|
||||
result.type = "statusPageMatchedDomain";
|
||||
result.statusPageSlug = StatusPage.domainMappingList[hostname];
|
||||
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
|
||||
} else {
|
||||
result.type = "entryPage";
|
||||
result.entryPage = server.entryPage;
|
||||
@@ -48,7 +38,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = parseFloat(request.query.ping) || null;
|
||||
let ping = parseInt(request.query.ping) || null;
|
||||
let statusString = request.query.status || "up";
|
||||
let status = (statusString === "up") ? UP : DOWN;
|
||||
|
||||
|
@@ -5,7 +5,7 @@ const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const Monitor = require("../model/monitor");
|
||||
const { badgeConstants } = require("../../src/util");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
|
||||
let router = express.Router();
|
||||
|
@@ -48,16 +48,7 @@ if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
|
||||
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
|
||||
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
|
||||
}
|
||||
|
||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||
|
||||
if (process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass") {
|
||||
log.warn("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
|
||||
}
|
||||
|
||||
log.info("server", "Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
@@ -84,18 +75,15 @@ const notp = require("notp");
|
||||
const base32 = require("thirty-two");
|
||||
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
const server = UptimeKumaServer.getInstance(args);
|
||||
const io = module.exports.io = server.io;
|
||||
const app = server.app;
|
||||
|
||||
log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
const User = require("./model/user");
|
||||
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
|
||||
} = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@@ -118,13 +106,19 @@ const passwordHash = require("./password-hash");
|
||||
const checkVersion = require("./check-version");
|
||||
log.info("server", "Version: " + checkVersion.version);
|
||||
|
||||
const hostname = config.hostname;
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = FBSD ? null : process.env.HOST;
|
||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
if (hostname) {
|
||||
log.info("server", "Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = config.port;
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||
@@ -302,11 +296,6 @@ let needSetup = false;
|
||||
]);
|
||||
|
||||
if (user) {
|
||||
// Check if the password changed
|
||||
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
|
||||
throw new Error("The token is invalid due to password change or old token");
|
||||
}
|
||||
|
||||
log.debug("auth", "afterLogin");
|
||||
afterLogin(socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
@@ -326,10 +315,9 @@ let needSetup = false;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Invalid token. IP=${clientIP}`);
|
||||
if (error.message) {
|
||||
log.error("auth", error.message, `IP=${clientIP}`);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid token.",
|
||||
@@ -368,7 +356,9 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: User.createJWT(user, server.jwtSecret),
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -396,7 +386,9 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: User.createJWT(user, server.jwtSecret),
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -648,10 +640,6 @@ let needSetup = false;
|
||||
let notificationIDList = 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);
|
||||
delete monitor.accepted_statuscodes;
|
||||
|
||||
@@ -668,10 +656,7 @@ let needSetup = false;
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
if (monitor.active !== false) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
}
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||
|
||||
@@ -717,11 +702,6 @@ let needSetup = false;
|
||||
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.description = monitor.description;
|
||||
bean.parent = monitor.parent;
|
||||
@@ -732,12 +712,6 @@ let needSetup = false;
|
||||
bean.headers = monitor.headers;
|
||||
bean.basic_auth_user = monitor.basic_auth_user;
|
||||
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 = monitor.oauth_auth_method;
|
||||
bean.oauth_token_url = monitor.oauth_token_url;
|
||||
bean.oauth_scopes = monitor.oauth_scopes;
|
||||
bean.tlsCa = monitor.tlsCa;
|
||||
bean.tlsCert = monitor.tlsCert;
|
||||
bean.tlsKey = monitor.tlsKey;
|
||||
@@ -791,10 +765,6 @@ let needSetup = false;
|
||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
|
||||
bean.kafkaProducerAllowAutoTopicCreation =
|
||||
monitor.kafkaProducerAllowAutoTopicCreation;
|
||||
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||
|
||||
bean.validate();
|
||||
|
||||
@@ -806,7 +776,7 @@ let needSetup = false;
|
||||
|
||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||
|
||||
if (await bean.isActive()) {
|
||||
if (bean.isActive()) {
|
||||
await restartMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
@@ -1126,6 +1096,9 @@ let needSetup = false;
|
||||
value,
|
||||
]);
|
||||
|
||||
// Cleanup unused Tags
|
||||
await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0");
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
@@ -1154,8 +1127,6 @@ let needSetup = false;
|
||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||
await user.resetPassword(password.newPassword);
|
||||
|
||||
server.disconnectAllSocketClients(user.id, socket.id);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
@@ -1206,7 +1177,6 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
const previousNSCDStatus = await Settings.get("nscd");
|
||||
|
||||
await setSettings("general", data);
|
||||
server.entryPage = data.entryPage;
|
||||
@@ -1224,15 +1194,6 @@ let needSetup = false;
|
||||
await resetChrome();
|
||||
}
|
||||
|
||||
// Update nscd status
|
||||
if (previousNSCDStatus !== data.nscd) {
|
||||
if (data.nscd) {
|
||||
await server.startNSCDServices();
|
||||
} else {
|
||||
await server.stopNSCDServices();
|
||||
}
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
@@ -1402,7 +1363,6 @@ let needSetup = false;
|
||||
|
||||
// Define default values
|
||||
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
|
||||
@@ -1428,7 +1388,6 @@ let needSetup = false;
|
||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||
authWorkstation: monitorListData[i].authWorkstation,
|
||||
authDomain: monitorListData[i].authDomain,
|
||||
timeout,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
resendInterval: monitorListData[i].resendInterval || 0,
|
||||
@@ -1630,8 +1589,6 @@ let needSetup = false;
|
||||
await shutdownFunction();
|
||||
});
|
||||
|
||||
server.start();
|
||||
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
log.info("server", `Listening on ${hostname}:${port}`);
|
||||
@@ -1836,7 +1793,6 @@ async function pauseMonitor(userID, monitorID) {
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
server.monitorList[monitorID].active = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1895,10 +1851,8 @@ gracefulShutdown(server.httpServer, {
|
||||
});
|
||||
|
||||
// Catch unexpected errors here
|
||||
let unexpectedErrorHandler = (error, promise) => {
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
UptimeKumaServer.errorLog(error, false);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
};
|
||||
process.addListener("unhandledRejection", unexpectedErrorHandler);
|
||||
process.addListener("uncaughtException", unexpectedErrorHandler);
|
||||
});
|
||||
|
@@ -42,50 +42,24 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
});
|
||||
|
||||
socket.on("getGameList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
gameList: getGameList(),
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
callback({
|
||||
ok: true,
|
||||
gameList: getGameList(),
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("testChrome", (executable, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
// 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,
|
||||
});
|
||||
// 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) {
|
||||
}).catch((e) => {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect all other socket clients of the user
|
||||
socket.on("disconnectOtherSocketClients", async () => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||
} catch (e) {
|
||||
log.warn("disconnectAllSocketClients", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -162,7 +162,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
statusPage.footer_text = config.footerText;
|
||||
statusPage.custom_css = config.customCSS;
|
||||
statusPage.show_powered_by = config.showPoweredBy;
|
||||
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
||||
statusPage.modified_date = R.isoDateTime();
|
||||
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
||||
|
||||
|
@@ -4,15 +4,12 @@ const fs = require("fs");
|
||||
const http = require("http");
|
||||
const { Server } = require("socket.io");
|
||||
const { R } = require("redbean-node");
|
||||
const { log, isDev } = require("../src/util");
|
||||
const { log } = require("../src/util");
|
||||
const Database = require("./database");
|
||||
const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
const path = require("path");
|
||||
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
@@ -63,17 +60,22 @@ class UptimeKumaServer {
|
||||
*/
|
||||
jwtSecret = null;
|
||||
|
||||
static getInstance() {
|
||||
static getInstance(args) {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer();
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
}
|
||||
return UptimeKumaServer.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor(args) {
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||
|
||||
log.info("server", "Creating express and socket.io instance");
|
||||
this.app = express();
|
||||
if (isSSL) {
|
||||
if (sslKey && sslCert) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
@@ -97,68 +99,8 @@ class UptimeKumaServer {
|
||||
|
||||
// Set Monitor Types
|
||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
|
||||
// Allow all CORS origins (polling) in development
|
||||
let cors = undefined;
|
||||
if (isDev) {
|
||||
cors = {
|
||||
origin: "*",
|
||||
};
|
||||
}
|
||||
|
||||
this.io = new Server(this.httpServer, {
|
||||
cors,
|
||||
allowRequest: async (req, callback) => {
|
||||
let transport;
|
||||
// It should be always true, but just in case, because this property is not documented
|
||||
if (req._query) {
|
||||
transport = req._query.transport;
|
||||
} else {
|
||||
log.error("socket", "Ops!!! Cannot get transport type, assume that it is polling");
|
||||
transport = "polling";
|
||||
}
|
||||
|
||||
const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers);
|
||||
log.info("socket", `New ${transport} connection, IP = ${clientIP}`);
|
||||
|
||||
// The following check is only for websocket connections, polling connections are already protected by CORS
|
||||
if (transport === "polling") {
|
||||
callback(null, true);
|
||||
} else if (transport === "websocket") {
|
||||
const bypass = process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
|
||||
if (bypass) {
|
||||
log.info("auth", "WebSocket origin check is bypassed");
|
||||
callback(null, true);
|
||||
} else if (!req.headers.origin) {
|
||||
log.info("auth", "WebSocket with no origin is allowed");
|
||||
callback(null, true);
|
||||
} else {
|
||||
let host = req.headers.host;
|
||||
let origin = req.headers.origin;
|
||||
|
||||
try {
|
||||
let originURL = new URL(origin);
|
||||
let xForwardedFor;
|
||||
if (await Settings.get("trustProxy")) {
|
||||
xForwardedFor = req.headers["x-forwarded-for"];
|
||||
}
|
||||
|
||||
if (host !== originURL.host && xForwardedFor !== originURL.host) {
|
||||
callback(null, false);
|
||||
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`);
|
||||
} else {
|
||||
callback(null, true);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid origin url, probably not from browser
|
||||
callback(null, false);
|
||||
log.error("auth", `Invalid origin url (${origin}), IP: ${clientIP}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
/** Initialise app after the database has been set up */
|
||||
@@ -270,7 +212,7 @@ class UptimeKumaServer {
|
||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||
*/
|
||||
static errorLog(error, outputToConsole = true) {
|
||||
const errorLogStream = fs.createWriteStream(path.join(Database.dataDir, "/error.log"), {
|
||||
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
|
||||
@@ -293,28 +235,20 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Get the IP of the client connected to the socket
|
||||
* @param {Socket} socket
|
||||
* @returns {Promise<string>}
|
||||
* @returns {string}
|
||||
*/
|
||||
getClientIP(socket) {
|
||||
return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers);
|
||||
}
|
||||
async getClientIP(socket) {
|
||||
let clientIP = socket.client.conn.remoteAddress;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} clientIP
|
||||
* @param {IncomingHttpHeaders} headers
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getClientIPwithProxy(clientIP, headers) {
|
||||
if (clientIP === undefined) {
|
||||
clientIP = "";
|
||||
}
|
||||
|
||||
if (await Settings.get("trustProxy")) {
|
||||
const forwardedFor = headers["x-forwarded-for"];
|
||||
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
|
||||
|
||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||
|| headers["x-real-ip"]
|
||||
|| socket.client.conn.request.headers["x-real-ip"]
|
||||
|| clientIP.replace(/^::ffff:/, "");
|
||||
} else {
|
||||
return clientIP.replace(/^::ffff:/, "");
|
||||
@@ -399,76 +333,9 @@ class UptimeKumaServer {
|
||||
dayjs.tz.setDefault(timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Listen logic should be moved to here
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start() {
|
||||
let enable = await Settings.get("nscd");
|
||||
|
||||
if (enable || enable === null) {
|
||||
await this.startNSCDServices();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
/** Stop the server */
|
||||
async stop() {
|
||||
let enable = await Settings.get("nscd");
|
||||
|
||||
if (enable || enable === null) {
|
||||
await this.stopNSCDServices();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all system services (e.g. nscd)
|
||||
* For now, only used in Docker
|
||||
*/
|
||||
async startNSCDServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Starting nscd");
|
||||
await childProcessAsync.exec("sudo service nscd start");
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to start nscd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all system services
|
||||
*/
|
||||
async stopNSCDServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Stopping nscd");
|
||||
await childProcessAsync.exec("sudo service nscd stop");
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to stop nscd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force connected sockets of a user to refresh and disconnect.
|
||||
* Used for resetting password.
|
||||
* @param {string} userID
|
||||
* @param {string?} currentSocketID
|
||||
*/
|
||||
disconnectAllSocketClients(userID, currentSocketID = undefined) {
|
||||
for (const socket of this.io.sockets.sockets.values()) {
|
||||
if (socket.userID === userID && socket.id !== currentSocketID) {
|
||||
try {
|
||||
socket.emit("refresh");
|
||||
socket.disconnect();
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,4 +345,3 @@ module.exports = {
|
||||
|
||||
// Must be at the end to avoid circular dependencies
|
||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const ping = require("@louislam/ping");
|
||||
const { R } = require("redbean-node");
|
||||
const { log, genSecret, badgeConstants } = require("../src/util");
|
||||
const { log, genSecret } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { Resolver } = require("dns");
|
||||
const childProcess = require("child_process");
|
||||
@@ -9,6 +9,7 @@ const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const mqtt = require("mqtt");
|
||||
const chroma = require("chroma-js");
|
||||
const { badgeConstants } = require("./config");
|
||||
const mssql = require("mssql");
|
||||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
@@ -20,9 +21,6 @@ const grpc = require("@grpc/grpc-js");
|
||||
const protojs = require("protobufjs");
|
||||
const radiusClient = require("node-radius-client");
|
||||
const redis = require("redis");
|
||||
const oidc = require("openid-client");
|
||||
const tls = require("tls");
|
||||
|
||||
const {
|
||||
dictionaries: {
|
||||
rfc2865: { file, attributes },
|
||||
@@ -33,7 +31,6 @@ const dayjs = require("dayjs");
|
||||
// SASLOptions used in JSDoc
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { Kafka, SASLOptions } = require("kafkajs");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
/**
|
||||
@@ -55,43 +52,6 @@ exports.initJWTSecret = async () => {
|
||||
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
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
@@ -287,22 +247,22 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
|
||||
|
||||
producer.connect().then(
|
||||
() => {
|
||||
producer.send({
|
||||
topic: topic,
|
||||
messages: [{
|
||||
value: message,
|
||||
}],
|
||||
}).then((_) => {
|
||||
try {
|
||||
producer.send({
|
||||
topic: topic,
|
||||
messages: [{
|
||||
value: message,
|
||||
}],
|
||||
});
|
||||
connectedToKafka = true;
|
||||
clearTimeout(timeoutID);
|
||||
resolve("Message sent successfully");
|
||||
}).catch((e) => {
|
||||
} catch (e) {
|
||||
connectedToKafka = true;
|
||||
producer.disconnect();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Error sending message: " + e.message));
|
||||
}).finally(() => {
|
||||
connectedToKafka = true;
|
||||
clearTimeout(timeoutID);
|
||||
});
|
||||
}
|
||||
}
|
||||
).catch(
|
||||
(e) => {
|
||||
@@ -314,10 +274,8 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa
|
||||
);
|
||||
|
||||
producer.on("producer.network.request_timeout", (_) => {
|
||||
if (!connectedToKafka) {
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.network.request_timeout"));
|
||||
}
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.network.request_timeout"));
|
||||
});
|
||||
|
||||
producer.on("producer.disconnect", (_) => {
|
||||
@@ -395,9 +353,6 @@ exports.mssqlQuery = async function (connectionString, query) {
|
||||
try {
|
||||
pool = new mssql.ConnectionPool(connectionString);
|
||||
await pool.connect();
|
||||
if (!query) {
|
||||
query = "SELECT 1";
|
||||
}
|
||||
await pool.request().query(query);
|
||||
pool.close();
|
||||
} catch (e) {
|
||||
@@ -418,22 +373,12 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = postgresConParse(connectionString);
|
||||
|
||||
// Fix #3868, which true/false is not parsed to boolean
|
||||
if (typeof config.ssl === "string") {
|
||||
config.ssl = config.ssl === "true";
|
||||
}
|
||||
|
||||
if (config.password === "") {
|
||||
// See https://github.com/brianc/node-postgres/issues/1927
|
||||
reject(new Error("Password is undefined."));
|
||||
return;
|
||||
return reject(new Error("Password is undefined."));
|
||||
}
|
||||
const client = new Client(config);
|
||||
|
||||
client.on("error", (error) => {
|
||||
log.debug("postgres", "Error caught in the error event handler.");
|
||||
reject(error);
|
||||
});
|
||||
const client = new Client({ connectionString });
|
||||
|
||||
client.connect((err) => {
|
||||
if (err) {
|
||||
@@ -457,7 +402,6 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
client.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -469,15 +413,11 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
* Run a query on MySQL/MariaDB
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @param {?string} password The password to use
|
||||
* @returns {Promise<(string)>}
|
||||
*/
|
||||
exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
||||
exports.mysqlQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection({
|
||||
uri: connectionString,
|
||||
password
|
||||
});
|
||||
const connection = mysql.createConnection(connectionString);
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
@@ -546,7 +486,6 @@ exports.radius = function (
|
||||
host: hostname,
|
||||
hostPort: port,
|
||||
timeout: timeout,
|
||||
retries: 1,
|
||||
dictionaries: [ file ],
|
||||
});
|
||||
|
||||
@@ -558,12 +497,6 @@ exports.radius = function (
|
||||
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||
],
|
||||
}).catch((error) => {
|
||||
if (error.response?.code) {
|
||||
throw Error(error.response.code);
|
||||
} else {
|
||||
throw Error(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -741,6 +674,7 @@ exports.checkCertificate = function (res) {
|
||||
* @param {number} status The status code to check
|
||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||
* @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) {
|
||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||
@@ -748,11 +682,6 @@ exports.checkStatusCode = function (status, 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));
|
||||
if (codeRangeSplit.length === 1) {
|
||||
if (status === codeRangeSplit[0]) {
|
||||
@@ -763,8 +692,7 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
log.error("monitor", `${codeRange} is not a valid status code range`);
|
||||
continue;
|
||||
throw new Error("Invalid status code range");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,80 +1001,3 @@ module.exports.grpcQuery = async (options) => {
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of SHA256 fingerprints for all known root certificates.
|
||||
* @returns {Set} A set of SHA256 fingerprints.
|
||||
*/
|
||||
module.exports.rootCertificatesFingerprints = () => {
|
||||
let fingerprints = tls.rootCertificates.map(cert => {
|
||||
let certLines = cert.split("\n");
|
||||
certLines.shift();
|
||||
certLines.pop();
|
||||
let certBody = certLines.join("");
|
||||
let buf = Buffer.from(certBody, "base64");
|
||||
|
||||
const shasum = crypto.createHash("sha256");
|
||||
shasum.update(buf);
|
||||
|
||||
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
|
||||
});
|
||||
|
||||
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
|
||||
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
|
||||
|
||||
return new Set(fingerprints);
|
||||
};
|
||||
|
||||
module.exports.SHAKE256_LENGTH = 16;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
* @param {number} len
|
||||
* @return {string}
|
||||
*/
|
||||
module.exports.shake256 = (data, len) => {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
return crypto.createHash("shake256", { outputLength: len })
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
};
|
||||
|
||||
// For unit test, export functions
|
||||
if (process.env.TEST_BACKEND) {
|
||||
module.exports.__test = {
|
||||
parseCertificateInfo,
|
||||
};
|
||||
module.exports.__getPrivateFunction = (functionName) => {
|
||||
return module.exports.__test[functionName];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an abort signal with the specified timeout.
|
||||
* @param {number} timeoutMs - The timeout in milliseconds.
|
||||
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
|
||||
*/
|
||||
module.exports.axiosAbortSignal = (timeoutMs) => {
|
||||
try {
|
||||
// Just in case, as 0 timeout here will cause the request to be aborted immediately
|
||||
if (!timeoutMs || timeoutMs <= 0) {
|
||||
timeoutMs = 5000;
|
||||
}
|
||||
return AbortSignal.timeout(timeoutMs);
|
||||
} catch (_) {
|
||||
// v16-: AbortSignal.timeout is not supported
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
setTimeout(() => abortController.abort(), timeoutMs);
|
||||
|
||||
return abortController.signal;
|
||||
} catch (_) {
|
||||
// v15-: AbortController is not supported
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -111,10 +111,6 @@ optgroup {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
|
||||
@@ -162,26 +158,6 @@ optgroup {
|
||||
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) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
@@ -460,6 +436,7 @@ optgroup {
|
||||
.monitor-list {
|
||||
&.scrollbar {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 107px);
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
|
@@ -8,9 +8,9 @@
|
||||
:placeholder="placeholder"
|
||||
:disabled="!enabled"
|
||||
>
|
||||
<button type="button" class="btn btn-outline-primary" :aria-label="actionAriaLabel" @click="action()">
|
||||
<a class="btn btn-outline-primary" @click="action()">
|
||||
<font-awesome-icon :icon="icon" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,13 +66,6 @@ export default {
|
||||
action: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
/**
|
||||
* The aria-label of the action button
|
||||
*/
|
||||
actionAriaLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: [ "update:modelValue" ],
|
||||
|
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div class="input-group mb-3">
|
||||
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
||||
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
|
||||
<font-awesome-icon :icon="icon" aria-hidden="true" />
|
||||
</button>
|
||||
</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 id of the form which will be targeted by a <label for=..
|
||||
*/
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 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: () => {},
|
||||
},
|
||||
/**
|
||||
* The aria-label of the action button
|
||||
*/
|
||||
actionAriaLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* Whether the action button is disabled.
|
||||
* @example true
|
||||
*/
|
||||
actionDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* Whether the select field is required.
|
||||
* @example true
|
||||
*/
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [ "update:modelValue" ],
|
||||
computed: {
|
||||
/**
|
||||
* Send value update to parent on change.
|
||||
*/
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -135,7 +135,7 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
import { badgeConstants } from "../util.ts";
|
||||
import { default as serverConfig } from "../../server/config.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -230,7 +230,7 @@ export default {
|
||||
"labelColor",
|
||||
],
|
||||
},
|
||||
badgeConstants,
|
||||
badgeConstants: serverConfig.badgeConstants,
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -1,56 +0,0 @@
|
||||
<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>
|
@@ -70,7 +70,7 @@ export default {
|
||||
Confirm,
|
||||
},
|
||||
props: {},
|
||||
emits: [ "added", "deleted" ],
|
||||
emits: [ "added" ],
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
@@ -167,7 +167,6 @@ export default {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$emit("deleted", this.id);
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
|
@@ -5,24 +5,15 @@
|
||||
v-for="(beat, index) in shortBeatList"
|
||||
:key="index"
|
||||
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"
|
||||
:title="getBeatTitle(beat)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!$root.isMobile && 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -65,30 +56,8 @@ 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() {
|
||||
if (!this.beatList) {
|
||||
if (! this.beatList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -146,53 +115,6 @@ 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: {
|
||||
beatList: {
|
||||
@@ -211,14 +133,14 @@ export default {
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.heartbeatList === null) {
|
||||
if (!(this.monitorId in this.$root.heartbeatList)) {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.size !== "big") {
|
||||
if (this.size === "small") {
|
||||
this.beatWidth = 5;
|
||||
this.beatHeight = 16;
|
||||
this.beatMargin = 2;
|
||||
@@ -229,11 +151,11 @@ export default {
|
||||
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
||||
const actualMargin = this.beatMargin * window.devicePixelRatio;
|
||||
|
||||
if (!Number.isInteger(actualWidth)) {
|
||||
if (! Number.isInteger(actualWidth)) {
|
||||
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(actualMargin)) {
|
||||
if (! Number.isInteger(actualMargin)) {
|
||||
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
|
||||
}
|
||||
|
||||
@@ -307,21 +229,4 @@ 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>
|
||||
|
@@ -5,18 +5,18 @@
|
||||
<h1 class="h3 mb-3 fw-normal" />
|
||||
|
||||
<div v-if="!tokenRequired" class="form-floating">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" autocomplete="username" required>
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||
<label for="floatingInput">{{ $t("Username") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!tokenRequired" class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password" required>
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="tokenRequired">
|
||||
<div class="form-floating mt-3">
|
||||
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456" autocomplete="one-time-code" required>
|
||||
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||
<label for="otp">{{ $t("Token") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -29,10 +29,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
startDateTime() {
|
||||
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
},
|
||||
endDateTime() {
|
||||
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -2,10 +2,6 @@
|
||||
<div class="shadow-box mb-3" :style="boxStyle">
|
||||
<div class="list-header">
|
||||
<div class="header-top">
|
||||
<button class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
|
||||
{{ $t("Select") }}
|
||||
</button>
|
||||
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
<a v-if="searchText == ''" class="search-icon">
|
||||
@@ -16,10 +12,7 @@
|
||||
</a>
|
||||
<form>
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control search-input"
|
||||
:placeholder="$t('Search...')"
|
||||
:aria-label="$t('Search monitored sites')"
|
||||
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</form>
|
||||
@@ -28,55 +21,27 @@
|
||||
<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 ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
|
||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<MonitorListItem
|
||||
v-for="(item, index) in sortedMonitorList"
|
||||
:key="index"
|
||||
:monitor="item"
|
||||
:showPathName="filtersActive"
|
||||
:isSelectMode="selectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
:deselect="deselect"
|
||||
v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item"
|
||||
:isSearch="searchText !== ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
|
||||
{{ $t("pauseMonitorMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import MonitorListItem from "../components/MonitorListItem.vue";
|
||||
import MonitorListFilter from "./MonitorListFilter.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
MonitorListItem,
|
||||
MonitorListFilter,
|
||||
},
|
||||
@@ -89,10 +54,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
selectMode: false,
|
||||
selectAll: false,
|
||||
disableSelectAllWatcher: false,
|
||||
selectedMonitors: {},
|
||||
windowTop: 0,
|
||||
filterState: {
|
||||
status: null,
|
||||
@@ -120,68 +81,31 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a sorted list of monitors based on the applied filters and search text.
|
||||
*
|
||||
* @return {Array} The sorted list of monitors.
|
||||
*/
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result = result.filter(monitor => {
|
||||
// filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
result = result.filter(monitor => monitor.parent === null);
|
||||
}
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === false) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === false) {
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -199,68 +123,28 @@ export default {
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
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;
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
result.map(monitor => {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
result = result.filter(monitor => this.filterState.status.includes(monitor.status));
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
if (!this.disableSelectAllWatcher) {
|
||||
this.selectedMonitors = {};
|
||||
|
||||
if (this.selectAll) {
|
||||
this.sortedMonitorList.forEach((item) => {
|
||||
this.selectedMonitors[item.id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.disableSelectAllWatcher = false;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
result = result.filter(monitor => this.filterState.active.includes(monitor.active));
|
||||
}
|
||||
},
|
||||
selectMode() {
|
||||
if (!this.selectMode) {
|
||||
this.selectAll = false;
|
||||
this.selectedMonitors = {};
|
||||
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
result = result.filter(monitor => {
|
||||
return 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;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -297,53 +181,6 @@ export default {
|
||||
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>
|
||||
@@ -434,12 +271,4 @@ export default {
|
||||
padding-left: 67px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.selection-controls {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -258,10 +258,6 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-filters-btn {
|
||||
font-size: 0.8em;
|
||||
margin-right: 5px;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div tabindex="-1" class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
|
||||
<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>
|
||||
@@ -44,7 +44,6 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
@import "../assets/app.scss";
|
||||
|
||||
.filter-dropdown-menu {
|
||||
z-index: 100;
|
||||
@@ -103,22 +102,17 @@ export default {
|
||||
}
|
||||
|
||||
.filter-dropdown-status {
|
||||
@extend .btn-outline-normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
color: $link-color;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 25px;
|
||||
background-color: transparent;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-font-color2;
|
||||
}
|
||||
border: 1px solid $dark-font-color2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
@@ -1,56 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="depthMargin">
|
||||
<!-- Checkbox -->
|
||||
<div v-if="isSelectMode" class="select-input-wrapper">
|
||||
<input
|
||||
class="form-check-input select-input"
|
||||
type="checkbox"
|
||||
:aria-label="$t('Check/Uncheck')"
|
||||
:checked="isSelected(monitor.id)"
|
||||
@click.stop="toggleSelection"
|
||||
/>
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info" :style="depthMargin">
|
||||
<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 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>
|
||||
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||
<div class="row">
|
||||
<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 v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12 bottom-style">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<transition name="slide-fade-up">
|
||||
<div v-if="!isCollapsed" class="childs">
|
||||
<MonitorListItem
|
||||
v-for="(item, index) in sortedChildMonitorList"
|
||||
:key="index" :monitor="item"
|
||||
:showPathName="showPathName"
|
||||
:isSelectMode="isSelectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
:deselect="deselect"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -75,13 +53,8 @@ export default {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Should the monitor name show it's parent */
|
||||
showPathName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** If the user is in select mode */
|
||||
isSelectMode: {
|
||||
/** If the user is currently searching */
|
||||
isSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@@ -90,21 +63,6 @@ export default {
|
||||
type: Number,
|
||||
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() {
|
||||
return {
|
||||
@@ -153,19 +111,13 @@ export default {
|
||||
};
|
||||
},
|
||||
monitorName() {
|
||||
if (this.showPathName) {
|
||||
if (this.isSearch) {
|
||||
return this.monitor.pathName;
|
||||
} else {
|
||||
return this.monitor.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isSelectMode() {
|
||||
// TODO: Resize the heartbeat bar, but too slow
|
||||
// this.$refs.heartbeatBar.resize();
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
||||
// Always unfold if monitor is accessed directly
|
||||
@@ -212,16 +164,6 @@ export default {
|
||||
monitorURL(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>
|
||||
@@ -259,14 +201,4 @@ export default {
|
||||
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>
|
||||
|
@@ -126,7 +126,6 @@ export default {
|
||||
"lunasea": "LunaSea",
|
||||
"matrix": "Matrix",
|
||||
"mattermost": "Mattermost",
|
||||
"nostr": "Nostr",
|
||||
"ntfy": "Ntfy",
|
||||
"octopush": "Octopush",
|
||||
"OneBot": "OneBot",
|
||||
@@ -158,7 +157,6 @@ export default {
|
||||
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
|
||||
"DingDing": "DingDing (钉钉自定义机器人)",
|
||||
"Feishu": "Feishu (飞书)",
|
||||
"FlashDuty": "FlashDuty (快猫星云)",
|
||||
"FreeMobile": "FreeMobile (mobile.free.fr)",
|
||||
"PushDeer": "PushDeer",
|
||||
"promosms": "PromoSMS",
|
||||
|
@@ -61,17 +61,12 @@
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="extra-info">
|
||||
<div v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining">
|
||||
<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 v-if="showTags" class="tags">
|
||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,10 +103,6 @@ export default {
|
||||
/** Should tags be shown? */
|
||||
showTags: {
|
||||
type: Boolean,
|
||||
},
|
||||
/** Should expiry be shown? */
|
||||
showCertificateExpiry: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -163,33 +154,6 @@ export default {
|
||||
}
|
||||
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>
|
||||
@@ -197,15 +161,6 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
.extra-info {
|
||||
display: flex;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.extra-info > div > div:first-child {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.no-monitor-msg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@@ -28,7 +28,6 @@
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -44,7 +43,7 @@
|
||||
<div v-if="uri && twoFAStatus == false" class="mt-3">
|
||||
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
|
||||
<div class="input-group">
|
||||
<input v-model="token" type="text" maxlength="6" class="form-control" autocomplete="one-time-code" required>
|
||||
<input v-model="token" type="text" maxlength="6" class="form-control">
|
||||
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
|
||||
</div>
|
||||
<p v-show="tokenValid" class="mt-2" style="color: green;">{{ $t("tokenValidSettingsMsg") }}</p>
|
||||
|
@@ -1,29 +0,0 @@
|
||||
<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,7 +1,9 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
|
||||
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
|
||||
<div class="input-group mb-3">
|
||||
<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">
|
||||
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
|
||||
</i18n-t>
|
||||
|
@@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<option value="ios">iOS</option>
|
||||
<option value="android">Android</option>
|
||||
<option value="android">{{ $t("Android") }}</option>
|
||||
<option value="huawei">{{ $t("Huawei") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@@ -5,7 +5,9 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
@@ -9,7 +9,10 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<p style="margin-top: 8px;">
|
||||
|
@@ -13,7 +13,7 @@
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||
<a href="https://developers.mattermost.com/integrate/webhooks/incoming/" target="_blank">https://developers.mattermost.com/integrate/webhooks/incoming/</a>
|
||||
<a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutMattermostChannelName") }}
|
||||
|
@@ -1,26 +0,0 @@
|
||||
<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,27 +1,19 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-ntfytopic" class="form-label">{{ $t("ntfy Topic") }}</label>
|
||||
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||
<div class="input-group mb-3">
|
||||
<input id="ntfy-ntfytopic" v-model="$parent.notification.ntfytopic" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||
<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 class="input-group mb-3">
|
||||
<input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
|
||||
<div class="form-text">
|
||||
<p v-if="$parent.notification.ntfyPriority >= 5">
|
||||
{{ $t("ntfyPriorityHelptextAllEvents") }}
|
||||
</p>
|
||||
<i18n-t v-else tag="p" keypath="ntfyPriorityHelptextAllExceptDown">
|
||||
<code>DOWN</code>
|
||||
<code>{{ $parent.notification.ntfyPriority + 1 }}</code>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>
|
||||
@@ -31,15 +23,21 @@
|
||||
</div>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
|
||||
<div class="input-group mb-3">
|
||||
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
|
||||
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
|
||||
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
|
||||
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
|
||||
|
@@ -1,9 +1,4 @@
|
||||
<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">
|
||||
<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>
|
||||
|
@@ -6,7 +6,9 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
@@ -19,13 +19,11 @@ import LineNotify from "./LineNotify.vue";
|
||||
import LunaSea from "./LunaSea.vue";
|
||||
import Matrix from "./Matrix.vue";
|
||||
import Mattermost from "./Mattermost.vue";
|
||||
import Nostr from "./Nostr.vue";
|
||||
import Ntfy from "./Ntfy.vue";
|
||||
import Octopush from "./Octopush.vue";
|
||||
import OneBot from "./OneBot.vue";
|
||||
import Opsgenie from "./Opsgenie.vue";
|
||||
import PagerDuty from "./PagerDuty.vue";
|
||||
import FlashDuty from "./FlashDuty.vue";
|
||||
import PagerTree from "./PagerTree.vue";
|
||||
import PromoSMS from "./PromoSMS.vue";
|
||||
import Pushbullet from "./Pushbullet.vue";
|
||||
@@ -79,13 +77,11 @@ const NotificationFormList = {
|
||||
"lunasea": LunaSea,
|
||||
"matrix": Matrix,
|
||||
"mattermost": Mattermost,
|
||||
"nostr": Nostr,
|
||||
"ntfy": Ntfy,
|
||||
"octopush": Octopush,
|
||||
"OneBot": OneBot,
|
||||
"Opsgenie": Opsgenie,
|
||||
"PagerDuty": PagerDuty,
|
||||
"FlashDuty": FlashDuty,
|
||||
"PagerTree": PagerTree,
|
||||
"promosms": PromoSMS,
|
||||
"pushbullet": Pushbullet,
|
||||
|
@@ -112,53 +112,6 @@
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@@ -150,43 +150,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Cache (nscd) -->
|
||||
<div v-if="$root.info.isContainer" class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("enableNSCD") }}
|
||||
</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="nscdEnable"
|
||||
v-model="settings.nscd"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="nscd"
|
||||
:value="true"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="nscdEnable">
|
||||
{{ $t("Enable") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="nscdDisable"
|
||||
v-model="settings.nscd"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="nscd"
|
||||
:value="false"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="nscdDisable">
|
||||
{{ $t("Disable") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Cache -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
|
@@ -27,13 +27,13 @@
|
||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)">
|
||||
<font-awesome-icon icon="times" />
|
||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
||||
<font-awesome-icon class="" icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||
|
@@ -19,7 +19,6 @@
|
||||
v-model="password.currentPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -33,7 +32,6 @@
|
||||
v-model="password.newPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -48,7 +46,6 @@
|
||||
type="password"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': invalidPassword }"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
|
@@ -455,6 +455,8 @@
|
||||
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
||||
"Device Token": "رمز الجهاز",
|
||||
"Platform": "منصة",
|
||||
"iOS": "iOS",
|
||||
"Android": "ذكري المظهر",
|
||||
"Huawei": "هواوي",
|
||||
"High": "عالٍ",
|
||||
"Retry": "إعادة المحاولة",
|
||||
|
@@ -592,6 +592,7 @@
|
||||
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
||||
"Device Token": "رمز الجهاز",
|
||||
"Platform": "منصة",
|
||||
"Android": "ذكري المظهر",
|
||||
"Huawei": "هواوي",
|
||||
"High": "عالٍ",
|
||||
"Retry": "إعادة المحاولة",
|
||||
@@ -683,6 +684,5 @@
|
||||
"languageName": "العربية",
|
||||
"Game": "الألعاب",
|
||||
"List": "القائمة",
|
||||
"statusMaintenance": "الصيانة",
|
||||
"Home": "الرئيسة"
|
||||
"statusMaintenance": "الصيانة"
|
||||
}
|
||||
|
@@ -333,7 +333,7 @@
|
||||
"Post": "Публикувай",
|
||||
"Please input title and content": "Моля, въведете заглавие и съдържание",
|
||||
"Created": "Създаден",
|
||||
"Last Updated": "Последно обновена",
|
||||
"Last Updated": "Последно обновен",
|
||||
"Unpin": "Откачи",
|
||||
"Switch to Light Theme": "Превключи към светла тема",
|
||||
"Switch to Dark Theme": "Превключи към тъмна тема",
|
||||
@@ -396,6 +396,8 @@
|
||||
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
|
||||
"Device Token": "Токен за устройство",
|
||||
"Platform": "Платформа",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "Висок",
|
||||
"Retry": "Повтори",
|
||||
@@ -643,8 +645,8 @@
|
||||
"smseaglePriority": "Приоритет на съобщението (0-9, по подразбиране = 0)",
|
||||
"IconUrl": "Икона URL адрес",
|
||||
"webhookAdditionalHeadersTitle": "Допълнителни хедъри",
|
||||
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката. Всеки хедър трябва да бъде дефиниран като JSON ключ/стойност.",
|
||||
"Enable DNS Cache": "Активирай DNS кеширане за HTTP(S) монитори",
|
||||
"webhookAdditionalHeadersDesc": "Задава допълнителни хедъри, изпратени с уеб куката.",
|
||||
"Enable DNS Cache": "Активирай DNS кеширане",
|
||||
"Enable": "Активирай",
|
||||
"Disable": "Деактивирай",
|
||||
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
|
||||
@@ -738,7 +740,7 @@
|
||||
"lunaseaDeviceID": "ID на устройството",
|
||||
"lunaseaUserID": "ID на потребител",
|
||||
"twilioAccountSID": "Профил SID",
|
||||
"twilioAuthToken": "Удостоверяващ токен / Тайна на API ключа",
|
||||
"twilioAuthToken": "Удостоверяващ токен",
|
||||
"twilioFromNumber": "От номер",
|
||||
"twilioToNumber": "Към номер",
|
||||
"sameAsServerTimezone": "Kато часовата зона на сървъра",
|
||||
@@ -747,7 +749,7 @@
|
||||
"cronSchedule": "График: ",
|
||||
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
|
||||
"cronExpression": "Израз тип \"Cron\"",
|
||||
"statusPageRefreshIn": "Ще се обнови след: {0}",
|
||||
"statusPageRefreshIn": "Обновяване след: {0}",
|
||||
"ntfyUsernameAndPassword": "Потребителско име и парола",
|
||||
"ntfyAuthenticationMethod": "Метод за удостоверяване",
|
||||
"pushoverMessageTtl": "TTL на съобщението (секунди)",
|
||||
@@ -755,7 +757,7 @@
|
||||
"Badge Generator": "Генератор на баджове на {0}",
|
||||
"Badge Type": "Тип бадж",
|
||||
"Badge Duration": "Продължителност на баджа",
|
||||
"Badge Prefix": "Префикс за стйността на баджа",
|
||||
"Badge Prefix": "Префикс на баджа",
|
||||
"Badge Label Color": "Цвят на етикета на баджа",
|
||||
"Badge Color": "Цвят на баджа",
|
||||
"Badge Label Suffix": "Суфикс на етикета на значката",
|
||||
@@ -769,9 +771,9 @@
|
||||
"Badge URL": "URL адрес на баджа",
|
||||
"Monitor Setting": "Настройка на монитор {0}",
|
||||
"Show Clickable Link": "Покажи връзка, която може да се кликне",
|
||||
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва мониторирания URL адрес.",
|
||||
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва URL адреса на монитора.",
|
||||
"Badge Label": "Етикет на баджа",
|
||||
"Badge Suffix": "Суфикс за стойността на баджа",
|
||||
"Badge Suffix": "Суфикс на баджа",
|
||||
"Badge Label Prefix": "Префикс на етикета на значката",
|
||||
"Badge Pending Color": "Цвят на баджа за изчакващ",
|
||||
"Badge Down Days": "Колко дни баджът да не се показва",
|
||||
@@ -782,61 +784,5 @@
|
||||
"Edit Maintenance": "Редактиране на поддръжка",
|
||||
"Home": "Главна страница",
|
||||
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
|
||||
"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} секунди",
|
||||
"styleElapsedTime": "Изминало време под лентата с проверки",
|
||||
"styleElapsedTimeShowNoLine": "Покажи (без ред)",
|
||||
"gamedigGuessPort": "Gamedig: Познай порт",
|
||||
"gamedigGuessPortDescription": "Портът, използван от Valve Server Query Protocol, може да е различен от клиентския порт. Опитайте това, ако мониторът не може да се свърже с вашия сървър.",
|
||||
"styleElapsedTimeShowWithLine": "Покажи (с ред)"
|
||||
"Close": "Затвори"
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
{}
|
@@ -18,13 +18,13 @@
|
||||
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
|
||||
"Start of maintenance": "Zahájit údržbu",
|
||||
"All Status Pages": "Všechny stavové stránky",
|
||||
"Select status pages...": "Vyberte stavové stránky…",
|
||||
"Select status pages...": "Vyberte stavovou stránku…",
|
||||
"recurringIntervalMessage": "Spustit jednou každý den | Spustit jednou každých {0} dní",
|
||||
"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",
|
||||
"atLeastOneMonitor": "Vyberte alespoň jeden dotčený dohled",
|
||||
"passwordNotMatchMsg": "Hesla se neshodují.",
|
||||
"notificationDescription": "Aby oznámení fungovala, je nutné jej přiřadit k dohledu.",
|
||||
"notificationDescription": "Aby byla upozornění fungovalo, je nutné ho 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.",
|
||||
"pauseDashboardHome": "Pauza",
|
||||
"deleteMonitorMsg": "Opravdu chcete odstranit tento dohled?",
|
||||
@@ -208,7 +208,7 @@
|
||||
"Status Page": "Stavová stránka",
|
||||
"Status Pages": "Stavová stránka",
|
||||
"defaultNotificationName": "Moje {notification} upozornění ({číslo})",
|
||||
"here": "klikněte sem",
|
||||
"here": "sem",
|
||||
"Required": "Vyžadováno",
|
||||
"telegram": "Telegram",
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
@@ -225,7 +225,7 @@
|
||||
"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}",
|
||||
"webhookAdditionalHeadersTitle": "Dodatečné hlavičky",
|
||||
"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.",
|
||||
"webhookAdditionalHeadersDesc": "Nastavte dodatečné hlavičky, které se odešlou společně s webhookem.",
|
||||
"smtp": "E-mail (SMTP)",
|
||||
"secureOptionNone": "Žádné / STARTTLS (25, 587)",
|
||||
"secureOptionTLS": "TLS (465)",
|
||||
@@ -243,8 +243,8 @@
|
||||
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
|
||||
"teams": "Microsoft Teams",
|
||||
"Webhook URL": "URL adresa webhooku",
|
||||
"wayToGetTeamsURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
|
||||
"wayToGetZohoCliqURL": "Pro informace o tom, jak vytvořit URL adresu webhooku {0}.",
|
||||
"wayToGetTeamsURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
|
||||
"wayToGetZohoCliqURL": "Informace o tom, jak vytvořit URL adresu webhooku naleznete na {0}.",
|
||||
"signal": "Signal",
|
||||
"Number": "Číslo",
|
||||
"Recipients": "Příjemci",
|
||||
@@ -339,7 +339,7 @@
|
||||
"PasswordsDoNotMatch": "Hesla se neshodují.",
|
||||
"records": "záznamů",
|
||||
"One record": "Jeden záznam",
|
||||
"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: ",
|
||||
"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: ",
|
||||
"Current User": "Aktuálně přihlášený uživatel",
|
||||
"topic": "Téma",
|
||||
"topicExplanation": "MQTT téma, které chcete sledovat",
|
||||
@@ -454,6 +454,8 @@
|
||||
"For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key",
|
||||
"Device Token": "Token zařízení",
|
||||
"Platform": "Platforma",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "Vysoký",
|
||||
"Retry": "Opakovat",
|
||||
@@ -525,8 +527,8 @@
|
||||
"RadiusCalledStationIdDescription": "Identifikátor volaného zařízení",
|
||||
"RadiusCallingStationId": "ID volajícího zařízení",
|
||||
"RadiusCallingStationIdDescription": "Identifikátor volajícího zařízení",
|
||||
"Certificate Expiry Notification": "Upozornění na blížící se konec platnosti certifikátu",
|
||||
"API Username": "Uživatelské jméno API",
|
||||
"Certificate Expiry Notification": "Oznámení na blížící se konec platnosti certifikátu",
|
||||
"API Username": "Uživatelské jména API",
|
||||
"API Key": "API klíč",
|
||||
"Recipient Number": "Číslo příjemce",
|
||||
"From Name/Number": "Jméno/číslo odesílatele",
|
||||
@@ -567,7 +569,7 @@
|
||||
"Also check beta release": "Kontrolovat také dostupnost beta verzí",
|
||||
"Using a Reverse Proxy?": "Používáte reverzní proxy?",
|
||||
"Check how to config it for WebSocket": "Zjistěte, jak ji nakonfigurovat pro WebSockety",
|
||||
"Steam Game Server": "Herní server ve službě Steam",
|
||||
"Steam Game Server": "Steam Game Server",
|
||||
"Most likely causes:": "Nejčastější důvody:",
|
||||
"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.",
|
||||
@@ -638,7 +640,7 @@
|
||||
"dayOfWeek": "Den v týdnu",
|
||||
"dayOfMonth": "Den v měsíci",
|
||||
"lastDay": "Poslední den",
|
||||
"lastDay1": "Poslední den v měsíci",
|
||||
"lastDay1": "1. poslední den v měsíci",
|
||||
"lastDay2": "2. poslední den v měsíci",
|
||||
"lastDay3": "3. poslední den v měsíci",
|
||||
"lastDay4": "4. poslední den v měsíci",
|
||||
@@ -653,10 +655,10 @@
|
||||
"Server Timezone": "Časové pásmo serveru",
|
||||
"statusPageMaintenanceEndDate": "Konec",
|
||||
"IconUrl": "Adresa URL ikony",
|
||||
"Enable DNS Cache": "Povolit DNS Cache pro HTTP(s) dohledy",
|
||||
"Enable DNS Cache": "Povolit DNS Cache",
|
||||
"Enable": "Povolit",
|
||||
"Disable": "Zakázat",
|
||||
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, tuto možnost vypněte.",
|
||||
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
|
||||
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
|
||||
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
|
||||
"Effective Date Range": "Časové období (volitelné)",
|
||||
@@ -690,7 +692,7 @@
|
||||
"uninstall": "Odinstalace",
|
||||
"uninstalling": "Odinstalování",
|
||||
"Packet Size": "Velikost paketu",
|
||||
"markdownSupported": "Markdown syntaxe je podporována",
|
||||
"markdownSupported": "Markdown syntaxe podporována",
|
||||
"Google Analytics ID": "ID Google Analytics",
|
||||
"Edit Tag": "Upravit štítek",
|
||||
"Server Address": "Adresa serveru",
|
||||
@@ -741,102 +743,46 @@
|
||||
"twilioAccountSID": "SID účtu",
|
||||
"twilioFromNumber": "Číslo odesílatele",
|
||||
"twilioToNumber": "Číslo příjemce",
|
||||
"twilioAuthToken": "Autorizační token / Tajemství API klíče",
|
||||
"twilioAuthToken": "Autorizační token",
|
||||
"sameAsServerTimezone": "Stejné jako časové pásmo serveru",
|
||||
"cronExpression": "Cron výraz",
|
||||
"cronSchedule": "Plán: ",
|
||||
"invalidCronExpression": "Neplatný cron výraz: {0}",
|
||||
"startDateTime": "Datum/čas začátku",
|
||||
"startDateTime": "Počáteční datum/čas",
|
||||
"endDateTime": "Datum/čas konce",
|
||||
"ntfyAuthenticationMethod": "Způsob ověření",
|
||||
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
|
||||
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
|
||||
"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 dohledu.",
|
||||
"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.",
|
||||
"Open Badge Generator": "Otevřít generátor odznaků",
|
||||
"Badge Type": "Typ odznaku",
|
||||
"Badge Duration": "Platnost odznaku",
|
||||
"Badge Label": "Štítek odznaku",
|
||||
"Badge Prefix": "Prefix hodnoty odznaku",
|
||||
"Monitor Setting": "Nastavení dohledu pro {0}",
|
||||
"Badge Prefix": "Prefix odznaku",
|
||||
"Monitor Setting": "{0}'s Nastavení dohledu",
|
||||
"Badge Generator": "Generátor odznaků pro {0}",
|
||||
"Badge Label Color": "Barva štítku odznaku",
|
||||
"Badge Color": "Barva odznaku",
|
||||
"Badge Style": "Styl odznaku",
|
||||
"Badge Label Suffix": "Přípona štítku odznaku",
|
||||
"Badge URL": "URL odznaku",
|
||||
"Badge Suffix": "Přípona hodnoty odznaku",
|
||||
"Badge Suffix": "Přípona odznaku",
|
||||
"Badge Label Prefix": "Prefix štítku odznaku",
|
||||
"Badge Up Color": "Barva odznaku při Běží",
|
||||
"Badge Down Color": "Barva odznaku při Nedostupné",
|
||||
"Badge Pending Color": "Barva odznaku při Pauze",
|
||||
"Badge Maintenance Color": "Barva odznaku při Údržbě",
|
||||
"Badge Warn Color": "Barva odznaku při Upozornění",
|
||||
"Reconnecting...": "Obnovování spojení…",
|
||||
"Cannot connect to the socket server": "Nelze se připojit k socketu serveru",
|
||||
"Reconnecting...": "Obnovení spojení...",
|
||||
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
|
||||
"Edit Maintenance": "Upravit Údržbu",
|
||||
"Home": "Hlavní stránka",
|
||||
"Badge Down Days": "Odznak nedostupných dní",
|
||||
"Group": "Skupina",
|
||||
"Monitor Group": "Sledovaná skupina",
|
||||
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupinu dohledů.",
|
||||
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
|
||||
"Close": "Zavřít",
|
||||
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
|
||||
"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",
|
||||
"styleElapsedTime": "Čas uplynulý pod heartbeat ukazatelem",
|
||||
"styleElapsedTimeShowWithLine": "Zobrazit (s linkou)",
|
||||
"gamedigGuessPortDescription": "Port používaný protokolem Valve Server Query Protocol se může lišit od portu klienta. Pokud se monitor nemůže připojit k serveru, zkuste to.",
|
||||
"styleElapsedTimeShowNoLine": "Zobrazit (bez linky)",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port"
|
||||
"Badge Warn Days": "Odznak dní s upozorněním"
|
||||
}
|
||||
|
@@ -211,7 +211,7 @@
|
||||
"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:",
|
||||
"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",
|
||||
"Content Type": "Indholdstype",
|
||||
"webhookJsonDesc": "{0} er god til alle moderne HTTP-servere som f.eks Express.js",
|
||||
@@ -558,6 +558,7 @@
|
||||
"high": "høj",
|
||||
"Base URL": "Base URL",
|
||||
"Platform": "Platform",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"Retry": "Forsøg igen",
|
||||
"Topic": "Emne",
|
||||
|
@@ -48,7 +48,7 @@
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Prüfintervall",
|
||||
"Retries": "Wiederholungen",
|
||||
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
|
||||
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
||||
"Advanced": "Erweitert",
|
||||
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||
"Upside Down Mode": "Umgekehrter Modus",
|
||||
@@ -109,10 +109,10 @@
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
"pauseMonitorMsg": "Bist du sicher, dass du den Monitor pausieren möchtest?",
|
||||
"clearEventsMsg": "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
|
||||
"clearHeartbeatsMsg": "Bist du sicher, dass du alle Prüfintervalle für diesen Monitor löschen möchtest?",
|
||||
"clearHeartbeatsMsg": "Bist du sicher, dass du alle Statistiken für diesen Monitor löschen möchtest?",
|
||||
"Clear Data": "Lösche Daten",
|
||||
"Events": "Ereignisse",
|
||||
"Heartbeats": "Prüfintervalle",
|
||||
"Heartbeats": "Statistiken",
|
||||
"confirmClearStatisticsMsg": "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin-Konto",
|
||||
"Repeat Password": "Passwort erneut eingeben",
|
||||
@@ -218,7 +218,7 @@
|
||||
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
"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",
|
||||
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||
"Post URL": "Post URL",
|
||||
@@ -380,12 +380,12 @@
|
||||
"alertaAlertState": "Alarmstatus",
|
||||
"alertaRecoverState": "Wiederherstellungsstatus",
|
||||
"deleteStatusPageMsg": "Bist du sicher, dass du diese Status-Seite löschen willst?",
|
||||
"Proxies": "Proxys",
|
||||
"Proxies": "Proxies",
|
||||
"default": "Standard",
|
||||
"enabled": "Aktiviert",
|
||||
"setAsDefault": "Als Standard setzen",
|
||||
"deleteProxyMsg": "Bist du sicher, dass du diesen Proxy für alle Monitore löschen willst?",
|
||||
"proxyDescription": "Proxys müssen einem Monitor zugewiesen werden, um zu funktionieren.",
|
||||
"proxyDescription": "Proxies müssen einem Monitor zugewiesen werden, um zu funktionieren.",
|
||||
"enableProxyDescription": "Dieser Proxy wird keinen Effekt auf Monitor-Anfragen haben, bis er aktiviert ist. Du kannst ihn temporär von allen Monitoren nach Aktivierungsstatus deaktivieren.",
|
||||
"setAsDefaultProxyDescription": "Dieser Proxy wird standardmässig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immer noch für jeden Monitor einzeln deaktivieren.",
|
||||
"Certificate Chain": "Zertifikatskette",
|
||||
@@ -403,6 +403,8 @@
|
||||
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
||||
"Device Token": "Gerätetoken",
|
||||
"Platform": "Platform",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "Hoch",
|
||||
"Retry": "Wiederholungen",
|
||||
@@ -655,10 +657,10 @@
|
||||
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
|
||||
"markdownSupported": "Markdown-Syntax unterstützt",
|
||||
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden.",
|
||||
"Packet Size": "Paketgrösse",
|
||||
"IconUrl": "Symbol URL",
|
||||
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
|
||||
"Enable DNS Cache": "DNS Cache aktivieren",
|
||||
"Help": "Hilfe",
|
||||
"Game": "Spiel",
|
||||
"General Monitor Type": "Allgemeiner Monitortyp",
|
||||
@@ -743,7 +745,7 @@
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
||||
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
||||
"startDateTime": "Start Datum/Uhrzeit",
|
||||
@@ -756,8 +758,8 @@
|
||||
"Badge Type": "Badge Typ",
|
||||
"Badge Duration": "Badge Dauer",
|
||||
"Badge Label": "Badge Label",
|
||||
"Badge Prefix": "Badge Wert Präfix",
|
||||
"Badge Suffix": "Badge Wert Suffix",
|
||||
"Badge Prefix": "Badge Präfix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Label Color": "Badge Label Farbe",
|
||||
"Badge Color": "Badge Farbe",
|
||||
"Badge Label Prefix": "Badge Label Präfix",
|
||||
@@ -779,61 +781,5 @@
|
||||
"Group": "Gruppe",
|
||||
"Monitor Group": "Monitor Gruppe",
|
||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||
"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",
|
||||
"FlashDuty Severity": "Schweregrad",
|
||||
"nostrSender": "Privater Schlüssel des Absenders (nsec)",
|
||||
"nostrRecipientsHelp": "npub-Format, eine pro Zeile",
|
||||
"noOrBadCertificate": "Kein/schlechtes Zertifikat",
|
||||
"wayToGetFlashDutyKey": "Gehe zu Channel -> (Wähle einen Channel) -> Integrationen -> Neue Integration hinzufügen', füge ein 'Custom Event' hinzu, um eine Push-Adresse zu erhalten, und kopiere den Integrationsschlüssel in die Adresse. Für weitere Informationen besuche bitte",
|
||||
"nostrRelays": "Nostr relays",
|
||||
"nostrRelaysHelp": "Eine Relay-URL pro Zeile",
|
||||
"nostrRecipients": "Öffentliche Schlüssel des Empfängers (npub)",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"Request Timeout": "Zeitüberschreitung der Anfrage",
|
||||
"styleElapsedTimeShowNoLine": "Anzeigen (keine Zeile)",
|
||||
"styleElapsedTimeShowWithLine": "Anzeigen (mit Zeile)",
|
||||
"Select": "Auswählen",
|
||||
"selectedMonitorCount": "Ausgewählt: {0}",
|
||||
"PushDeer Server": "PushDeer Server",
|
||||
"showCertificateExpiry": "Ablauf des Zertifikats anzeigen",
|
||||
"gamedigGuessPortDescription": "Der vom Valve Server Query Protocol verwendete Port kann sich vom Port des Clients unterscheiden. Versuche dies, wenn der Monitor keine Verbindung zum Server herstellen kann.",
|
||||
"timeoutAfter": "Zeitüberschreitung nach {0} Sekunden",
|
||||
"styleElapsedTime": "Verstrichene Zeit unter der Prüfintervallleiste",
|
||||
"Check/Uncheck": "Aktivieren/Deaktivieren"
|
||||
"Close": "Schliessen"
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Prüfintervall",
|
||||
"Retries": "Wiederholungen",
|
||||
"retriesDescription": "Maximale Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird",
|
||||
"retriesDescription": "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
||||
"Advanced": "Erweitert",
|
||||
"ignoreTLSError": "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||
"Upside Down Mode": "Umgekehrter Modus",
|
||||
@@ -109,10 +109,10 @@
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
"pauseMonitorMsg": "Bist du sicher, dass du den Monitor pausieren möchtest?",
|
||||
"clearEventsMsg": "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
|
||||
"clearHeartbeatsMsg": "Bist du sicher, dass du alle Prüfintervalle für diesen Monitor löschen möchtest?",
|
||||
"clearHeartbeatsMsg": "Bist du sicher, dass du alle Statistiken für diesen Monitor löschen möchtest?",
|
||||
"Clear Data": "Lösche Daten",
|
||||
"Events": "Ereignisse",
|
||||
"Heartbeats": "Prüfintervalle",
|
||||
"Heartbeats": "Statistiken",
|
||||
"confirmClearStatisticsMsg": "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin-Konto",
|
||||
"Repeat Password": "Passwort erneut eingeben",
|
||||
@@ -218,7 +218,7 @@
|
||||
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
"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",
|
||||
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||
"Post URL": "Post URL",
|
||||
@@ -380,12 +380,12 @@
|
||||
"alertaAlertState": "Alarmstatus",
|
||||
"alertaRecoverState": "Wiederherstellungsstatus",
|
||||
"deleteStatusPageMsg": "Bist du sicher, dass du diese Status-Seite löschen willst?",
|
||||
"Proxies": "Proxys",
|
||||
"Proxies": "Proxies",
|
||||
"default": "Standard",
|
||||
"enabled": "Aktiviert",
|
||||
"setAsDefault": "Als Standard setzen",
|
||||
"deleteProxyMsg": "Bist du sicher, dass du diesen Proxy für alle Monitore löschen willst?",
|
||||
"proxyDescription": "Proxys müssen einem Monitor zugewiesen werden, um zu funktionieren.",
|
||||
"proxyDescription": "Proxies müssen einem Monitor zugewiesen werden, um zu funktionieren.",
|
||||
"enableProxyDescription": "Dieser Proxy wird keinen Effekt auf Monitor-Anfragen haben, bis er aktiviert ist. Du kannst ihn temporär von allen Monitoren nach Aktivierungsstatus deaktivieren.",
|
||||
"setAsDefaultProxyDescription": "Dieser Proxy wird standardmäßig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immer noch für jeden Monitor einzeln deaktivieren.",
|
||||
"Certificate Chain": "Zertifikatskette",
|
||||
@@ -403,6 +403,8 @@
|
||||
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
|
||||
"Device Token": "Gerätetoken",
|
||||
"Platform": "Platform",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "Hoch",
|
||||
"Retry": "Wiederholungen",
|
||||
@@ -644,11 +646,11 @@
|
||||
"Help": "Hilfe",
|
||||
"Game": "Spiel",
|
||||
"Custom": "Benutzerdefiniert",
|
||||
"Enable DNS Cache": "DNS-Cache für HTTP(s)-Monitore aktivieren",
|
||||
"Enable DNS Cache": "DNS-Cache aktivieren",
|
||||
"Enable": "Aktivieren",
|
||||
"Disable": "Deaktivieren",
|
||||
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden. Jede Kopfzeile sollte als JSON Schlüssel/Wert definiert werden.",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.",
|
||||
"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.",
|
||||
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
|
||||
@@ -700,7 +702,7 @@
|
||||
"Edit Tag": "bearbeite Tag",
|
||||
"Server Address": "Server Adresse",
|
||||
"Learn More": "Erfahre mehr",
|
||||
"Body Encoding": "Inhaltskodierung",
|
||||
"Body Encoding": "Körperkodierung",
|
||||
"Add API Key": "API Schlüssel hinzufügen",
|
||||
"apiKey-active": "Aktiv",
|
||||
"apiKey-expired": "Abgelaufen",
|
||||
@@ -747,7 +749,7 @@
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"statusPageRefreshIn": "Aktualisierung in: {0}",
|
||||
"sameAsServerTimezone": "Gleiche Zeitzone wie Server",
|
||||
"startDateTime": "Start Datum/Uhrzeit",
|
||||
@@ -776,67 +778,11 @@
|
||||
"Badge Pending Color": "Badge Pending Farbe",
|
||||
"Badge Down Days": "Badge Down Tage",
|
||||
"Monitor Setting": "{0}'s Monitor Einstellung",
|
||||
"Badge Prefix": "Badge Wert Präfix",
|
||||
"Badge Suffix": "Badge Wert Suffix",
|
||||
"Badge Prefix": "Badge Präfix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Warn Days": "Badge Warnung Tage",
|
||||
"Group": "Gruppe",
|
||||
"Monitor Group": "Monitor Gruppe",
|
||||
"noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
|
||||
"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",
|
||||
"FlashDuty Severity": "Schweregrad",
|
||||
"nostrRelays": "Nostr relays",
|
||||
"gamedigGuessPort": "Gamedig: Guess Port",
|
||||
"Request Timeout": "Zeitüberschreitung der Anfrage",
|
||||
"styleElapsedTimeShowNoLine": "Anzeigen (keine Zeile)",
|
||||
"Select": "Auswählen",
|
||||
"selectedMonitorCount": "Ausgewählt: {0}",
|
||||
"PushDeer Server": "PushDeer Server",
|
||||
"nostrRelaysHelp": "Eine Relay-URL pro Zeile",
|
||||
"nostrSender": "Privater Schlüssel des Absenders (nsec)",
|
||||
"gamedigGuessPortDescription": "Der vom Valve Server Query Protocol verwendete Port kann sich vom Port des Clients unterscheiden. Versuche dies, wenn der Monitor keine Verbindung zum Server herstellen kann.",
|
||||
"wayToGetFlashDutyKey": "Gehe zu Channel -> (Wähle einen Channel) -> Integrationen -> Neue Integration hinzufügen', füge ein 'Custom Event' hinzu, um eine Push-Adresse zu erhalten, und kopiere den Integrationsschlüssel in die Adresse. Für weitere Informationen besuche bitte",
|
||||
"timeoutAfter": "Zeitüberschreitung nach {0} Sekunden",
|
||||
"styleElapsedTimeShowWithLine": "Anzeigen (mit Zeile)",
|
||||
"styleElapsedTime": "Verstrichene Zeit unter der Prüfintervallleiste",
|
||||
"Check/Uncheck": "Aktivieren/Deaktivieren",
|
||||
"nostrRecipients": "Öffentliche Schlüssel des Empfängers (npub)",
|
||||
"nostrRecipientsHelp": "npub-Format, eine pro Zeile",
|
||||
"showCertificateExpiry": "Ablauf des Zertifikats anzeigen",
|
||||
"noOrBadCertificate": "Kein/schlechtes Zertifikat"
|
||||
"Close": "Schließen"
|
||||
}
|
||||
|
@@ -420,6 +420,8 @@
|
||||
"For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
|
||||
"Device Token": "Device Token",
|
||||
"Platform": "Platform",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "High",
|
||||
"Retry": "Ξαναδοκιμάσετε",
|
||||
|
@@ -59,8 +59,6 @@
|
||||
"Hostname": "Hostname",
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Heartbeat Interval",
|
||||
"Request Timeout": "Request Timeout",
|
||||
"timeoutAfter": "Timeout after {0} seconds",
|
||||
"Retries": "Retries",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively",
|
||||
@@ -87,9 +85,6 @@
|
||||
"Dark": "Dark",
|
||||
"Auto": "Auto",
|
||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||
"styleElapsedTime": "Elapsed time under the heartbeat bar",
|
||||
"styleElapsedTimeShowNoLine": "Show (No Line)",
|
||||
"styleElapsedTimeShowWithLine": "Show (With Line)",
|
||||
"Normal": "Normal",
|
||||
"Bottom": "Bottom",
|
||||
"None": "None",
|
||||
@@ -183,7 +178,6 @@
|
||||
"Pink": "Pink",
|
||||
"Custom": "Custom",
|
||||
"Search...": "Search…",
|
||||
"Search monitored sites": "Search monitored sites",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
@@ -275,9 +269,6 @@
|
||||
"Services": "Services",
|
||||
"Discard": "Discard",
|
||||
"Cancel": "Cancel",
|
||||
"Select": "Select",
|
||||
"selectedMonitorCount": "Selected: {0}",
|
||||
"Check/Uncheck": "Check/Uncheck",
|
||||
"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.",
|
||||
"Customize": "Customize",
|
||||
@@ -335,8 +326,6 @@
|
||||
"Fingerprint:": "Fingerprint:",
|
||||
"No status pages": "No status pages",
|
||||
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
||||
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||
"Remove the expiry notification": "Remove the expiry notification day",
|
||||
"Proxy": "Proxy",
|
||||
"Date Created": "Date Created",
|
||||
"Footer Text": "Footer Text",
|
||||
@@ -372,12 +361,9 @@
|
||||
"Setup Docker Host": "Setup Docker Host",
|
||||
"Connection Type": "Connection Type",
|
||||
"Docker Daemon": "Docker Daemon",
|
||||
"noDockerHostMsg": "Not Available. Setup a Docker Host First.",
|
||||
"DockerHostRequired": "Please set the Docker Host for this monitor.",
|
||||
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
||||
"socket": "Socket",
|
||||
"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",
|
||||
"Container Name / ID": "Container Name / ID",
|
||||
"Docker Host": "Docker Host",
|
||||
@@ -454,10 +440,9 @@
|
||||
"Server Timezone": "Server Timezone",
|
||||
"statusPageMaintenanceEndDate": "End",
|
||||
"IconUrl": "Icon URL",
|
||||
"Enable DNS Cache": "(Deprecated) Enable DNS Cache for HTTP(s) monitors",
|
||||
"Enable DNS Cache": "Enable DNS Cache",
|
||||
"Enable": "Enable",
|
||||
"Disable": "Disable",
|
||||
"enableNSCD": "Enable NSCD (Name Service Cache Daemon) for caching all DNS requests",
|
||||
"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.",
|
||||
@@ -634,6 +619,7 @@
|
||||
"For safety, must use secret key": "For safety, must use secret key",
|
||||
"Device Token": "Device Token",
|
||||
"Platform": "Platform",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"High": "High",
|
||||
"Retry": "Retry",
|
||||
@@ -659,10 +645,6 @@
|
||||
"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",
|
||||
"setup a new monitor group": "setup a new monitor group",
|
||||
"openModalTo": "open modal to {0}",
|
||||
"Add a domain": "Add a domain",
|
||||
"Remove domain": "Remove domain '{0}'",
|
||||
"Icon Emoji": "Icon Emoji",
|
||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||
"aboutWebhooks": "More info about Webhooks on: {0}",
|
||||
@@ -708,15 +690,12 @@
|
||||
"Octopush API Version": "Octopush API Version",
|
||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
||||
"ntfy Topic": "ntfy Topic",
|
||||
"Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic",
|
||||
"onebotHttpAddress": "OneBot HTTP Address",
|
||||
"onebotMessageType": "OneBot Message Type",
|
||||
"onebotGroupMessage": "Group",
|
||||
"onebotPrivateMessage": "Private",
|
||||
"onebotUserOrGroupId": "Group/User ID",
|
||||
"onebotSafetyTips": "For safety, must set access token",
|
||||
"PushDeer Server": "PushDeer Server",
|
||||
"pushDeerServerDescription": "Leave blank to use the official server",
|
||||
"PushDeer Key": "PushDeer Key",
|
||||
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
||||
"Custom Monitor Type": "Custom Monitor Type",
|
||||
@@ -756,8 +735,6 @@
|
||||
"lunaseaDeviceID": "Device ID",
|
||||
"lunaseaUserID": "User ID",
|
||||
"ntfyAuthenticationMethod": "Authentication Method",
|
||||
"ntfyPriorityHelptextAllEvents": "All events are send with the maximum priority",
|
||||
"ntfyPriorityHelptextAllExceptDown": "All events are send with this priority, except {0}-events, which have a priority of {1}",
|
||||
"ntfyUsernameAndPassword": "Username and Password",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioApiKey": "Api Key (optional)",
|
||||
@@ -807,16 +784,5 @@
|
||||
"Session Token": "Session Token",
|
||||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||
"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."
|
||||
"Request Body": "Request Body"
|
||||
}
|
||||
|
@@ -270,7 +270,7 @@
|
||||
"Display Timezone": "Mostrar Zona Horaria",
|
||||
"Server Timezone": "Servidor de Zona Horaria",
|
||||
"statusPageMaintenanceEndDate": "Finaliza",
|
||||
"Enable DNS Cache": "Habilitar Cache DNS de monitores HTTP(s)",
|
||||
"Enable DNS Cache": "Habilitar Cache DNS",
|
||||
"No Maintenance": "Sin Mantenimiento",
|
||||
"weekdayShortSun": "Dom",
|
||||
"dayOfWeek": "Día de la Semana",
|
||||
@@ -389,7 +389,7 @@
|
||||
"emojiCheatSheet": "Hoja de trucos Emoji: {0}",
|
||||
"webhookJsonDesc": "{0} es bueno para cualquier servidor HTTP moderno como Express.js",
|
||||
"webhookFormDataDesc": "{multipart} es bueno para PHP. El JSON deberá analizarse con {decodeFunction}",
|
||||
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook. Cada cabecera debe definirse como una clave/valor JSON.",
|
||||
"webhookAdditionalHeadersDesc": "Establece encabezados adicionales enviados con el webhook.",
|
||||
"appriseInstalled": "Apprise está instalado.",
|
||||
"successMessage": "Mensaje de éxito",
|
||||
"Pick Accepted Status Codes...": "Seleccione Códigos de Estado Aceptados…",
|
||||
@@ -497,6 +497,8 @@
|
||||
"Proto Method": "Método Proto",
|
||||
"Proto Content": "Contenido Proto",
|
||||
"Economy": "Económico",
|
||||
"iOS": "iOS",
|
||||
"Android": "Android",
|
||||
"Platform": "Plataforma",
|
||||
"onebotPrivateMessage": "Privado",
|
||||
"onebotMessageType": "Tipo de Mensaje OneBot",
|
||||
@@ -588,8 +590,8 @@
|
||||
"GoogleChat": "Chat de Google (sólo Google Workspace)",
|
||||
"Kook": "Kook",
|
||||
"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 el grupo para obtener su ID",
|
||||
"Guild ID": "ID de grupo",
|
||||
"wayToGetKookGuildID": "Activa 'Modo Desarrollador' en los ajustes de Kook, y haz click derecho en la unión para obtener su ID",
|
||||
"Guild ID": "ID de Gremio",
|
||||
"User Key": "Key de Usuario",
|
||||
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
|
||||
"octopushTypeLowCost": "Bajo Coste (Lento - algunas veces bloqueado por operador)",
|
||||
@@ -653,7 +655,7 @@
|
||||
"gorush": "Gorush",
|
||||
"squadcast": "Squadcast",
|
||||
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
|
||||
"Effective Date Range": "Rango de Fecha Efectivo (Opcional)",
|
||||
"Effective Date Range": "Rango de Fecha Efectivo",
|
||||
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
|
||||
"Gateway Type": "Tipo de Puerta de Enlace",
|
||||
"SMSManager": "SMSManager",
|
||||
@@ -696,7 +698,7 @@
|
||||
"High": "Alto",
|
||||
"alertaApiEndpoint": "Endpoint API",
|
||||
"Body Encoding": "Codificación del cuerpo",
|
||||
"Expiry date": "Fecha de vencimiento",
|
||||
"Expiry date": "Fecha de expiración",
|
||||
"Expiry": "Expiración",
|
||||
"API Keys": "Claves API",
|
||||
"Key Added": "Clave añadida",
|
||||
@@ -749,22 +751,7 @@
|
||||
"statusPageRefreshIn": "Reinicio en: {0}",
|
||||
"twilioAuthToken": "Token de Autentificación",
|
||||
"ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
|
||||
"ntfyAuthenticationMethod": "Método de Autenticación",
|
||||
"ntfyAuthenticationMethod": "Método de Autentificación",
|
||||
"Cannot connect to the socket server": "No se puede conectar al servidor socket",
|
||||
"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"
|
||||
"Reconnecting...": "Reconectando..."
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user