mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-24 06:49:41 -05:00
Merge branch 'master' into Revyn112_master
# Conflicts: # server/model/monitor.js # src/languages/en.js # src/pages/EditMonitor.vue
This commit is contained in:
commit
ae31eb6ba9
@ -19,7 +19,6 @@ README.md
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.github
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
app.json
|
||||
CODE_OF_CONDUCT.md
|
||||
@ -28,7 +27,8 @@ CNAME
|
||||
install.sh
|
||||
SECURITY.md
|
||||
tsconfig.json
|
||||
|
||||
.env
|
||||
/tmp
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
|
17
.eslintrc.js
17
.eslintrc.js
@ -91,6 +91,23 @@ module.exports = {
|
||||
"rules": {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
}
|
||||
},
|
||||
|
||||
// Override for jest puppeteer
|
||||
{
|
||||
"files": [
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.jsx"
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
globals: {
|
||||
page: true,
|
||||
browser: true,
|
||||
context: true,
|
||||
jestPuppeteer: true,
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
#patreon: # Replace with a single Patreon username
|
||||
open_collective: uptime-kuma # Replace with a single Open Collective username
|
||||
#ko_fi: # Replace with a single Ko-fi username
|
||||
|
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@ -9,6 +9,9 @@ assignees: ''
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Describe your problem**
|
||||
Please describe what you are asking for
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
|
35
.github/workflows/auto-test.yml
vendored
Normal file
35
.github/workflows/auto-test.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Auto Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
auto-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [14.x, 16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm run install-legacy
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
env:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ dist-ssr
|
||||
|
||||
/private
|
||||
/out
|
||||
/tmp
|
||||
.env
|
||||
|
130
CONTRIBUTING.md
130
CONTRIBUTING.md
@ -4,54 +4,73 @@ First of all, thank you everyone who made pull requests for Uptime Kuma, I never
|
||||
|
||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
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.
|
||||
|
||||
# Can I create a pull request for Uptime Kuma?
|
||||
## Key Technical Skills
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
|
||||
- Node.js (You should know what are promise, async/await and arrow function etc.)
|
||||
- Socket.io
|
||||
- SCSS
|
||||
- Vue.js
|
||||
- Bootstrap
|
||||
- SQLite
|
||||
|
||||
## Directories
|
||||
|
||||
- data (App data)
|
||||
- dist (Frontend build)
|
||||
- extra (Extra useful scripts)
|
||||
- public (Frontend resources for dev only)
|
||||
- server (Server source code)
|
||||
- src (Frontend source code)
|
||||
- test (unit test)
|
||||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
|
||||
|
||||
If you are not sure, feel free to create an empty pull request draft first.
|
||||
|
||||
## Pull Request Examples
|
||||
### Pull Request Examples
|
||||
|
||||
### ✅ High - Medium Priority
|
||||
#### ✅ High - Medium Priority
|
||||
|
||||
- Add a new notification
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
- Translations
|
||||
|
||||
### *️⃣ Requires one more reviewer
|
||||
#### *️⃣ Requires one more reviewer
|
||||
|
||||
I do not have such knowledge to test it.
|
||||
|
||||
- Add k8s supports
|
||||
|
||||
### *️⃣ Low Priority
|
||||
#### *️⃣ Low Priority
|
||||
|
||||
It changed my current workflow and require further studies.
|
||||
|
||||
- Change my release approach
|
||||
|
||||
### ❌ Won't Merge
|
||||
#### ❌ Won't Merge
|
||||
|
||||
- Duplicated pull request
|
||||
- Buggy
|
||||
- Existing logic is completely modified or deleted
|
||||
- A function that is completely out of scope
|
||||
|
||||
# Project Styles
|
||||
## Project Styles
|
||||
|
||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||
|
||||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||
|
||||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||
- All settings in frontend.
|
||||
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
|
||||
- Settings should be configurable in the frontend. Env var is not encouraged.
|
||||
- Easy to use
|
||||
|
||||
# Coding Styles
|
||||
## Coding Styles
|
||||
|
||||
- 4 spaces indentation
|
||||
- Follow `.editorconfig`
|
||||
- Follow ESLint
|
||||
|
||||
@ -61,26 +80,20 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
||||
- SQLite: underscore_type
|
||||
- CSS/SCSS: dash-type
|
||||
|
||||
# Tools
|
||||
## Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports EditorConfig and ESLint (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||
|
||||
# Install dependencies
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install --dev
|
||||
npm ci
|
||||
```
|
||||
|
||||
For npm@7, you need --legacy-peer-deps
|
||||
|
||||
```bash
|
||||
npm install --legacy-peer-deps --dev
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
## How to start the Backend Dev Server
|
||||
|
||||
(2021-09-23 Update)
|
||||
|
||||
@ -90,41 +103,39 @@ npm run start-server-dev
|
||||
|
||||
It binds to `0.0.0.0:3001` by default.
|
||||
|
||||
## Backend Details
|
||||
### Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
|
||||
# Frontend Dev
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (indivdual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- scoket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
|
||||
Start frontend dev server. Hot-reload enabled in this way. It binds to `0.0.0.0:3000` by default.
|
||||
## How to start the Frontend Dev Server
|
||||
|
||||
1. Set the env var `NODE_ENV` to "development".
|
||||
2. Start the frontend dev server by the following command.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
|
||||
It binds to `0.0.0.0:3000` by default.
|
||||
|
||||
You can use Vue.js devtools Chrome extension for debugging.
|
||||
|
||||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
|
||||
|
||||
```javascript
|
||||
localStorage.dev = "dev";
|
||||
```
|
||||
|
||||
So that the frontend will try to connect websocket server in 3001.
|
||||
|
||||
Alternately, you can specific `NODE_ENV` to "development".
|
||||
|
||||
## Build the frontend
|
||||
### Build the frontend
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Frontend Details
|
||||
### Frontend Details
|
||||
|
||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||
|
||||
@ -134,11 +145,36 @@ As you can see, most data in frontend is stored in root level, even though you c
|
||||
|
||||
The data and socket logic are in `src/mixins/socket.js`.
|
||||
|
||||
# Database Migration
|
||||
## Database Migration
|
||||
|
||||
1. Create `patch{num}.sql` in `./db/`
|
||||
2. Update `latestVersion` in `./server/database.js`
|
||||
1. Create `patch-{name}.sql` in `./db/`
|
||||
2. Add your patch filename in the `patchList` list in `./server/database.js`
|
||||
|
||||
# Unit Test
|
||||
## Unit Test
|
||||
|
||||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.
|
||||
It is an end-to-end testing. It is using Jest and Puppeteer.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
|
||||
|
||||
## Update Dependencies
|
||||
|
||||
Install `ncu`
|
||||
https://github.com/raineorshine/npm-check-updates
|
||||
|
||||
```bash
|
||||
ncu -u -t patch
|
||||
npm install
|
||||
```
|
||||
|
||||
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/))
|
||||
|
||||
## Translations
|
||||
|
||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
33
README.md
33
README.md
@ -1,6 +1,6 @@
|
||||
# Uptime Kuma
|
||||
|
||||
<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://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=Backers&color=brightgreen" /></a>
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
|
||||
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
|
||||
|
||||
## 🥔 Live Demo
|
||||
|
||||
@ -16,17 +16,20 @@ Try it!
|
||||
|
||||
https://demo.uptime.kuma.pet
|
||||
|
||||
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
|
||||
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
||||
|
||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
||||
* 20 seconds interval.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||
* 20 second intervals.
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
* Simple Status Page
|
||||
* Ping Chart
|
||||
* Certificate Info
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
@ -37,7 +40,7 @@ docker volume create uptime-kuma
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### 💪🏻 Without Docker
|
||||
|
||||
@ -55,11 +58,11 @@ npm run setup
|
||||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have: npm install pm2 -g
|
||||
# Install PM2 if you don't have it: npm install pm2 -g
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
@ -85,9 +88,13 @@ https://github.com/louislam/uptime-kuma/projects/1
|
||||
|
||||
## 🖼 More Screenshots
|
||||
|
||||
Dark Mode:
|
||||
Light Mode:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
|
||||
<img src="https://uptime.kuma.pet/img/light.jpg" width="512" alt="" />
|
||||
|
||||
Status Page:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/134628766-a3fe0981-0926-4285-ab46-891a21c3e4cb.png" width="512" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
@ -111,11 +118,13 @@ If you love this project, please consider giving me a ⭐.
|
||||
## 🗣️ Discussion
|
||||
|
||||
### Issues Page
|
||||
|
||||
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
### Subreddit
|
||||
|
||||
My Reddit account: louislamlam
|
||||
You can mention me if you ask question on Reddit.
|
||||
You can mention me if you ask a question on Reddit.
|
||||
https://www.reddit.com/r/UptimeKuma/
|
||||
|
||||
## Contribute
|
||||
|
18
SECURITY.md
18
SECURITY.md
@ -5,11 +5,27 @@
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
| 1.8.X | :white_check_mark: |
|
||||
| <= 1.7.X | ❌ |
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
| Tag | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1 | :white_check_mark: |
|
||||
| 1-debian | :white_check_mark: |
|
||||
| 1-alpine | :white_check_mark: |
|
||||
| latest | :white_check_mark: |
|
||||
| debian | :white_check_mark: |
|
||||
| alpine | :white_check_mark: |
|
||||
| All other tags | ❌ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to uptime@kuma.pet.
|
||||
|
||||
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||
|
11
babel.config.js
Normal file
11
babel.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
const config = {};
|
||||
|
||||
if (process.env.TEST_FRONTEND) {
|
||||
config.presets = ["@babel/preset-env"];
|
||||
}
|
||||
|
||||
if (process.env.TEST_BACKEND) {
|
||||
config.plugins = ["babel-plugin-rewire"];
|
||||
}
|
||||
|
||||
module.exports = config;
|
5
config/jest-backend.config.js
Normal file
5
config/jest-backend.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
"rootDir": "..",
|
||||
"testRegex": "./test/backend.spec.js",
|
||||
};
|
||||
|
5
config/jest-frontend.config.js
Normal file
5
config/jest-frontend.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
"rootDir": "..",
|
||||
"testRegex": "./test/frontend.spec.js",
|
||||
};
|
||||
|
6
config/jest-puppeteer.config.js
Normal file
6
config/jest-puppeteer.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
"launch": {
|
||||
"headless": process.env.HEADLESS_TEST || false,
|
||||
"userDataDir": "./data/test-chrome-profile",
|
||||
}
|
||||
};
|
11
config/jest.config.js
Normal file
11
config/jest.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
"verbose": true,
|
||||
"preset": "jest-puppeteer",
|
||||
"globals": {
|
||||
"__DEV__": true
|
||||
},
|
||||
"testRegex": "./test/e2e.spec.js",
|
||||
"rootDir": "..",
|
||||
"testTimeout": 30000,
|
||||
};
|
||||
|
24
config/vite.config.js
Normal file
24
config/vite.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
import legacy from "@vitejs/plugin-legacy";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ["ie > 11"],
|
||||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
|
||||
})
|
||||
],
|
||||
css: {
|
||||
postcss: {
|
||||
"parser": postCssScss,
|
||||
"map": false,
|
||||
"plugins": [postcssRTLCSS]
|
||||
}
|
||||
},
|
||||
});
|
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
@ -0,0 +1,13 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD method TEXT default 'GET' not null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD body TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD headers TEXT default null;
|
||||
|
||||
COMMIT;
|
7
db/patch-monitor-push_token.sql
Normal file
7
db/patch-monitor-push_token.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD push_token VARCHAR(20) DEFAULT NULL;
|
||||
|
||||
COMMIT;
|
8
docker/alpine-base.dockerfile
Normal file
8
docker/alpine-base.dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12
|
||||
WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
12
docker/debian-base.dockerfile
Normal file
12
docker/debian-base.dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim
|
||||
WORKDIR /app
|
||||
|
||||
# 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, specific --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /var/lib/apt/lists/*
|
51
docker/dockerfile
Normal file
51
docker/dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
FROM louislam/uptime-kuma:base-debian AS build
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM louislam/uptime-kuma:base-debian AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
||||
# Upload the artifact to Github
|
||||
FROM louislam/uptime-kuma:base-debian AS upload-artifact
|
||||
WORKDIR /
|
||||
RUN apt update && \
|
||||
apt --yes install curl file
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ARG TARGETARCH
|
||||
ARG PLATFORM=debian
|
||||
ARG VERSION
|
||||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
|
||||
ARG DIST=dist.tar.gz
|
||||
|
||||
COPY --from=build /app /app
|
||||
RUN chmod +x /app/extra/upload-github-release-asset.sh
|
||||
|
||||
# Full Build
|
||||
# RUN tar -zcvf $FILE app
|
||||
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE
|
||||
|
||||
# Dist only
|
||||
RUN cd /app && tar -zcvf $DIST dist
|
||||
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST
|
||||
|
26
docker/dockerfile-alpine
Normal file
26
docker/dockerfile-alpine
Normal file
@ -0,0 +1,26 @@
|
||||
FROM louislam/uptime-kuma:base-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||
|
||||
COPY . .
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm ci --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM louislam/uptime-kuma:base-alpine AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
33
dockerfile
33
dockerfile
@ -1,33 +0,0 @@
|
||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||
# If the image changed, the second stage image should be changed too
|
||||
FROM node:14-buster-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-buster-slim AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
RUN apt update && \
|
||||
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
@ -1,30 +0,0 @@
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
npm run build && \
|
||||
npm prune --production && \
|
||||
chmod +x /app/extra/entrypoint.sh
|
||||
|
||||
|
||||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise && \
|
||||
rm -rf /root/.cache
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
ENTRYPOINT ["extra/entrypoint.sh"]
|
||||
CMD ["node", "server/server.js"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
6
ecosystem.config.js
Normal file
6
ecosystem.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "uptime-kuma",
|
||||
script: "./server/server.js",
|
||||
}]
|
||||
}
|
57
extra/download-dist.js
Normal file
57
extra/download-dist.js
Normal file
@ -0,0 +1,57 @@
|
||||
console.log("Downloading dist");
|
||||
const https = require("https");
|
||||
const tar = require("tar");
|
||||
|
||||
const packageJSON = require("../package.json");
|
||||
const fs = require("fs");
|
||||
const version = packageJSON.version;
|
||||
|
||||
const filename = "dist.tar.gz";
|
||||
|
||||
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
|
||||
download(url);
|
||||
|
||||
function download(url) {
|
||||
console.log(url);
|
||||
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode === 200) {
|
||||
console.log("Extracting dist...");
|
||||
|
||||
if (fs.existsSync("./dist")) {
|
||||
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
fs.renameSync("./dist", "./dist-backup");
|
||||
}
|
||||
|
||||
const tarStream = tar.x({
|
||||
cwd: "./",
|
||||
});
|
||||
|
||||
tarStream.on("close", () => {
|
||||
fs.rmdirSync("./dist-backup", {
|
||||
recursive: true
|
||||
});
|
||||
console.log("Done");
|
||||
});
|
||||
|
||||
tarStream.on("error", () => {
|
||||
if (fs.existsSync("./dist-backup")) {
|
||||
fs.renameSync("./dist-backup", "./dist");
|
||||
}
|
||||
console.log("Done");
|
||||
});
|
||||
|
||||
response.pipe(tarStream);
|
||||
} else if (response.statusCode === 302) {
|
||||
download(response.headers.location);
|
||||
} else {
|
||||
console.log("dist not found");
|
||||
}
|
||||
});
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
|
||||
# set -e Exit the script if an error happens
|
||||
set -e
|
||||
PUID=${PUID=1000}
|
||||
PGID=${PGID=1000}
|
||||
PUID=${PUID=0}
|
||||
PGID=${PGID=0}
|
||||
|
||||
files_ownership () {
|
||||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
||||
|
@ -12,13 +12,14 @@ const rl = readline.createInterface({
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const main = async () => {
|
||||
Database.init(args);
|
||||
await Database.connect();
|
||||
|
||||
try {
|
||||
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
const user = await R.findOne("user");
|
||||
|
||||
if (! user) {
|
||||
throw new Error("user not found, have you installed?");
|
||||
}
|
||||
@ -35,27 +36,35 @@ const rl = readline.createInterface({
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
rl.close();
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Password reset successfully.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error: " + e.message);
|
||||
}
|
||||
|
||||
await Database.close();
|
||||
rl.close();
|
||||
|
||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
||||
})();
|
||||
console.log("Finished.");
|
||||
};
|
||||
|
||||
function question(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
};
|
||||
|
@ -26,10 +26,12 @@ const copyRecursiveSync = function (src, dest) {
|
||||
}
|
||||
};
|
||||
|
||||
console.log("Arguments:", process.argv)
|
||||
console.log("Arguments:", process.argv);
|
||||
const baseLangCode = process.argv[2] || "en";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
if (fs.existsSync("./languages")) {
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
}
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
|
||||
const en = (await import("./languages/en.js")).default;
|
||||
@ -39,7 +41,7 @@ console.log("Files:", files);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".js")) {
|
||||
console.log("Skipping " + file)
|
||||
console.log("Skipping " + file);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ if (! newVersion) {
|
||||
const exists = tagExists(newVersion);
|
||||
|
||||
if (! exists) {
|
||||
|
||||
// Process package.json
|
||||
pkg.version = newVersion;
|
||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||
@ -29,8 +30,11 @@ if (! exists) {
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
||||
updateWiki(oldVersion, newVersion);
|
||||
|
||||
} else {
|
||||
console.log("version exists")
|
||||
console.log("version exists");
|
||||
}
|
||||
|
||||
function commit(version) {
|
||||
@ -38,16 +42,16 @@ function commit(version) {
|
||||
|
||||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||
let stdout = res.stdout.toString().trim();
|
||||
console.log(stdout)
|
||||
console.log(stdout);
|
||||
|
||||
if (stdout.includes("no changes added to commit")) {
|
||||
throw new Error("commit error")
|
||||
throw new Error("commit error");
|
||||
}
|
||||
}
|
||||
|
||||
function tag(version) {
|
||||
let res = child_process.spawnSync("git", ["tag", version]);
|
||||
console.log(res.stdout.toString().trim())
|
||||
console.log(res.stdout.toString().trim());
|
||||
}
|
||||
|
||||
function tagExists(version) {
|
||||
@ -59,3 +63,38 @@ function tagExists(version) {
|
||||
|
||||
return res.stdout.toString().trim() === version;
|
||||
}
|
||||
|
||||
function updateWiki(oldVersion, newVersion) {
|
||||
const wikiDir = "./tmp/wiki";
|
||||
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
|
||||
|
||||
safeDelete(wikiDir);
|
||||
|
||||
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
|
||||
let content = fs.readFileSync(howToUpdateFilename).toString();
|
||||
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
|
||||
fs.writeFileSync(howToUpdateFilename, content);
|
||||
|
||||
child_process.spawnSync("git", ["add", "-A"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
console.log("Pushing to Github");
|
||||
child_process.spawnSync("git", ["push"], {
|
||||
cwd: wikiDir,
|
||||
});
|
||||
|
||||
safeDelete(wikiDir);
|
||||
}
|
||||
|
||||
function safeDelete(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmdirSync(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
64
extra/upload-github-release-asset.sh
Normal file
64
extra/upload-github-release-asset.sh
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Author: Stefan Buck
|
||||
# License: MIT
|
||||
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
|
||||
#
|
||||
#
|
||||
# This script accepts the following parameters:
|
||||
#
|
||||
# * owner
|
||||
# * repo
|
||||
# * tag
|
||||
# * filename
|
||||
# * github_api_token
|
||||
#
|
||||
# Script to upload a release asset using the GitHub API v3.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
|
||||
#
|
||||
|
||||
# Check dependencies.
|
||||
set -e
|
||||
xargs=$(which gxargs || which xargs)
|
||||
|
||||
# Validate settings.
|
||||
[ "$TRACE" ] && set -x
|
||||
|
||||
CONFIG=$@
|
||||
|
||||
for line in $CONFIG; do
|
||||
eval "$line"
|
||||
done
|
||||
|
||||
# Define variables.
|
||||
GH_API="https://api.github.com"
|
||||
GH_REPO="$GH_API/repos/$owner/$repo"
|
||||
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
||||
AUTH="Authorization: token $github_api_token"
|
||||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
||||
CURL_ARGS="-LJO#"
|
||||
|
||||
if [[ "$tag" == 'LATEST' ]]; then
|
||||
GH_TAGS="$GH_REPO/releases/latest"
|
||||
fi
|
||||
|
||||
# Validate token.
|
||||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
||||
|
||||
# Read asset tags.
|
||||
response=$(curl -sH "$AUTH" $GH_TAGS)
|
||||
|
||||
# Get ID of the asset based on given filename.
|
||||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
||||
|
||||
# Upload asset
|
||||
echo "Uploading asset... "
|
||||
|
||||
# Construct url
|
||||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
||||
|
||||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
|
12980
package-lock.json
generated
12980
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
152
package.json
152
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -15,20 +15,29 @@
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host",
|
||||
"dev": "vite --host --config ./config/vite.config.js",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
"build": "vite build",
|
||||
"build": "vite build --config ./config/vite.config.js",
|
||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||
"test-with-build": "npm run build && npm test",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
|
||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
|
||||
"tsc": "tsc",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
||||
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.0-alpine --target release . --push",
|
||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.0-debian --target release . --push",
|
||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
|
||||
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
|
||||
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.8.0-alpine --target release . --push",
|
||||
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.8.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.8.0-debian --target release . --push",
|
||||
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"setup": "git checkout 1.7.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@ -43,66 +52,77 @@
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@louislam/sqlite3": "^5.0.6",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.1.1",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "^3.6.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.5",
|
||||
"notp": "^2.0.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"prom-client": "^13.2.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
||||
"@louislam/sqlite3": "~6.0.0",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.21.4",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"chardet": "^1.3.0",
|
||||
"bree": "~6.3.1",
|
||||
"chart.js": "~3.5.1",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.7",
|
||||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"qrcode": "~1.4.4",
|
||||
"redbean-node": "0.1.2",
|
||||
"socket.io": "^4.2.0",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"timezones-list": "^3.0.1",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"socket.io": "~4.2.0",
|
||||
"socket.io-client": "~4.2.0",
|
||||
"tar": "^6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"timezones-list": "~3.0.1",
|
||||
"v-pagination-3": "~0.1.6",
|
||||
"vue": "next",
|
||||
"vue-chart-3": "^0.5.8",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-contenteditable": "^3.0.4",
|
||||
"vue-i18n": "^9.1.7",
|
||||
"vue-image-crop-upload": "^3.0.3",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"vue-qrcode": "^1.0.0",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
"vue-chart-3": "~0.5.8",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~9.1.9",
|
||||
"vue-image-crop-upload": "~3.0.3",
|
||||
"vue-multiselect": "~3.0.0-alpha.2",
|
||||
"vue-qrcode": "~1.0.0",
|
||||
"vue-router": "~4.0.11",
|
||||
"vue-toastification": "~2.0.0-rc.1",
|
||||
"vuedraggable": "~4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.7",
|
||||
"@types/bootstrap": "^5.1.6",
|
||||
"@vitejs/plugin-legacy": "^1.5.3",
|
||||
"@vitejs/plugin-vue": "^1.9.1",
|
||||
"@vue/compiler-sfc": "^3.2.16",
|
||||
"core-js": "^3.18.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dns2": "^2.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.18.0",
|
||||
"sass": "^1.42.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"vite": "^2.5.10"
|
||||
"@babel/eslint-parser": "~7.15.7",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.4",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"puppeteer": "~10.4.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~13.13.1",
|
||||
"stylelint-config-standard": "~22.0.0",
|
||||
"typescript": "~4.4.3",
|
||||
"vite": "~2.6.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
const { setSetting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const { isDev } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
@ -22,7 +21,6 @@ exports.startInterval = () => {
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
console.log("Latest Version: " + exports.latestVersion);
|
||||
} catch (_) { }
|
||||
|
||||
};
|
||||
|
@ -4,6 +4,8 @@
|
||||
const { TimeLogger } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { io } = require("./server");
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@ -14,10 +16,10 @@ async function sendNotificationList(socket) {
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.export())
|
||||
result.push(bean.export());
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
io.to(socket.userID).emit("notificationList", result);
|
||||
|
||||
timeLogger.print("Send Notification List");
|
||||
|
||||
@ -39,7 +41,7 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||
LIMIT 100
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
]);
|
||||
|
||||
let result = list.reverse();
|
||||
|
||||
@ -69,7 +71,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
LIMIT 500
|
||||
`, [
|
||||
monitorID,
|
||||
])
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||
|
||||
@ -81,8 +83,18 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
|
||||
}
|
||||
|
||||
async function sendInfo(socket) {
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
primaryBaseURL: await setting("primaryBaseURL")
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
}
|
||||
sendInfo
|
||||
};
|
||||
|
||||
|
7
server/config.js
Normal file
7
server/config.js
Normal file
@ -0,0 +1,7 @@
|
||||
const args = require("args-parser")(process.argv);
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
demoMode
|
||||
};
|
@ -49,6 +49,8 @@ class Database {
|
||||
"patch-add-apikey-monitor.sql": true,
|
||||
"patch-incident-table.sql": true,
|
||||
"patch-group-table.sql": true,
|
||||
"patch-monitor-push_token.sql": true,
|
||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +110,7 @@ class Database {
|
||||
R.freeze(true);
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
|
31
server/jobs.js
Normal file
31
server/jobs.js
Normal file
@ -0,0 +1,31 @@
|
||||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
|
||||
const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
interval: "at 03:14",
|
||||
}
|
||||
];
|
||||
|
||||
const initBackgroundJobs = function (args) {
|
||||
const bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
jobs,
|
||||
worker: {
|
||||
env: SHARE_ENV,
|
||||
workerData: args,
|
||||
},
|
||||
workerMessageHandler: (message) => {
|
||||
console.log("[Background Job]:", message);
|
||||
}
|
||||
});
|
||||
|
||||
bree.start();
|
||||
return bree;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initBackgroundJobs
|
||||
};
|
40
server/jobs/clear-old-data.js
Normal file
40
server/jobs/clear-old-data.js
Normal file
@ -0,0 +1,40 @@
|
||||
const { log, exit, connectDb } = require("./util-worker");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
|
||||
(async () => {
|
||||
await connectDb();
|
||||
|
||||
let period = await setting("keepDataPeriodDays");
|
||||
|
||||
// Set Default Period
|
||||
if (period == null) {
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
period = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
// Try parse setting
|
||||
let parsedPeriod;
|
||||
try {
|
||||
parsedPeriod = parseInt(period);
|
||||
} catch (_) {
|
||||
log("Failed to parse setting, resetting to default..");
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
try {
|
||||
await R.exec(
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[parsedPeriod]
|
||||
);
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
|
||||
exit();
|
||||
})();
|
39
server/jobs/util-worker.js
Normal file
39
server/jobs/util-worker.js
Normal file
@ -0,0 +1,39 @@
|
||||
const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
const exit = function (error) {
|
||||
if (error && error != 0) {
|
||||
process.exit(error);
|
||||
} else {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage("done");
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
);
|
||||
|
||||
Database.init({
|
||||
"data-dir": dbPath,
|
||||
});
|
||||
|
||||
await Database.connect();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
log,
|
||||
exit,
|
||||
connectDb,
|
||||
};
|
@ -11,7 +11,9 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@ -53,13 +55,15 @@ class Monitor extends BeanModel {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: this.active,
|
||||
type: this.type,
|
||||
apikey: this.apikey,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
keyword: this.keyword,
|
||||
@ -70,6 +74,7 @@ class Monitor extends BeanModel {
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
pushToken: this.pushToken,
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
};
|
||||
@ -136,11 +141,15 @@ class Monitor extends BeanModel {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
let res = await axios.get(this.url, {
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
@ -150,7 +159,8 @@ class Monitor extends BeanModel {
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
});
|
||||
};
|
||||
let res = await axios.request(options);
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
@ -166,7 +176,13 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
||||
console.log(res.data);
|
||||
}
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
@ -237,6 +253,25 @@ class Monitor extends BeanModel {
|
||||
|
||||
bean.msg = dnsMessage;
|
||||
bean.status = UP;
|
||||
} else if (this.type === "push") { // Type: Push
|
||||
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
|
||||
|
||||
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
|
||||
this.id,
|
||||
time
|
||||
]);
|
||||
|
||||
debug("heartbeatCount" + heartbeatCount + " " + time);
|
||||
|
||||
if (heartbeatCount <= 0) {
|
||||
throw new Error("No heartbeat in the time window");
|
||||
} else {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
retries = 0;
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (this.type === "steam") {
|
||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||
const filter = `addr\\${this.hostname}:${this.port}`;
|
||||
@ -277,6 +312,9 @@ class Monitor extends BeanModel {
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but server is not found");
|
||||
}
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
@ -304,61 +342,23 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
let beatInterval = this.interval;
|
||||
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else if (bean.status === PENDING) {
|
||||
if (this.retryInterval !== this.interval) {
|
||||
if (this.retryInterval > 0) {
|
||||
beatInterval = this.retryInterval;
|
||||
}
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
@ -375,12 +375,27 @@ class Monitor extends BeanModel {
|
||||
previousBeat = bean;
|
||||
|
||||
if (! this.isStop) {
|
||||
|
||||
if (demoMode) {
|
||||
if (beatInterval < 20) {
|
||||
console.log("beat interval too low, reset to 20s");
|
||||
beatInterval = 20;
|
||||
}
|
||||
}
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Delay Push Type
|
||||
if (this.type === "push") {
|
||||
setTimeout(() => {
|
||||
beat();
|
||||
}, this.interval * 1000);
|
||||
} else {
|
||||
beat();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
@ -541,6 +556,54 @@ class Monitor extends BeanModel {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
108
server/notification-providers/aliyun-sms.js
Normal file
108
server/notification-providers/aliyun-sms.js
Normal file
@ -0,0 +1,108 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const qs = require("qs");
|
||||
|
||||
class AliyunSMS extends NotificationProvider {
|
||||
name = "AliyunSMS";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let msgBody = JSON.stringify({
|
||||
name: monitorJSON["name"],
|
||||
time: heartbeatJSON["time"],
|
||||
status: this.statusToString(heartbeatJSON["status"]),
|
||||
msg: heartbeatJSON["msg"],
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let msgBody = JSON.stringify({
|
||||
name: "",
|
||||
time: "",
|
||||
status: "",
|
||||
msg: msg,
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
PhoneNumbers: notification.phonenumber,
|
||||
TemplateCode: notification.templateCode,
|
||||
SignName: notification.signName,
|
||||
TemplateParam: msgbody,
|
||||
AccessKeyId: notification.accessKeyId,
|
||||
Format: "JSON",
|
||||
SignatureMethod: "HMAC-SHA1",
|
||||
SignatureVersion: "1.0",
|
||||
SignatureNonce: Math.random().toString(),
|
||||
Timestamp: new Date().toISOString(),
|
||||
Action: "SendSms",
|
||||
Version: "2017-05-25",
|
||||
};
|
||||
|
||||
params.Signature = this.sign(params, notification.secretAccessKey);
|
||||
let config = {
|
||||
method: "POST",
|
||||
url: "http://dysmsapi.aliyuncs.com/",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: qs.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.Message == "OK") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Aliyun request sign */
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
let data = [];
|
||||
|
||||
let oa = Object.keys(param).sort();
|
||||
|
||||
for (let i = 0; i < oa.length; i++) {
|
||||
let key = oa[i];
|
||||
param2[key] = param[key];
|
||||
}
|
||||
|
||||
for (let key in param2) {
|
||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||
}
|
||||
|
||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||
return Crypto
|
||||
.createHmac("sha1", `${AccessKeySecret}&`)
|
||||
.update(Buffer.from(StringToSign))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AliyunSMS;
|
79
server/notification-providers/dingding.js
Normal file
79
server/notification-providers/dingding.js
Normal file
@ -0,0 +1,79 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
|
||||
class DingDing extends NotificationProvider {
|
||||
name = "DingDing";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let params = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: monitorJSON["name"],
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let params = {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: msg
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
|
||||
let config = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||
data: JSON.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.errmsg == "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** DingDing sign */
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DingDing;
|
83
server/notification-providers/feishu.js
Normal file
83
server/notification-providers/feishu.js
Normal file
@ -0,0 +1,83 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Feishu extends NotificationProvider {
|
||||
name = "Feishu";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let feishuWebHookUrl = notification.feishuWebHookUrl;
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
msg_type: "text",
|
||||
content: {
|
||||
text: msg,
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == DOWN) {
|
||||
let downdata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] == UP) {
|
||||
let updata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, updata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Feishu;
|
45
server/notification-providers/matrix.js
Normal file
45
server/notification-providers/matrix.js
Normal file
@ -0,0 +1,45 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const { debug } = require("../../src/util");
|
||||
|
||||
class Matrix extends NotificationProvider {
|
||||
name = "matrix";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
const size = 20;
|
||||
const randomString = encodeURIComponent(
|
||||
Crypto
|
||||
.randomBytes(size)
|
||||
.toString("base64")
|
||||
.slice(0, size)
|
||||
);
|
||||
|
||||
debug("Random String: " + randomString);
|
||||
|
||||
const roomId = encodeURIComponent(notification.internalRoomId);
|
||||
|
||||
debug("Matrix Room ID: " + roomId);
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${notification.accessToken}`,
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"msgtype": "m.text",
|
||||
"body": msg
|
||||
};
|
||||
|
||||
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Matrix;
|
@ -9,6 +9,8 @@ class Octopush extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
// Default - V2
|
||||
if (notification.octopushVersion == 2 || !notification.octopushVersion) {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
@ -28,8 +30,30 @@ class Octopush extends NotificationProvider {
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
} else if (notification.octopushVersion == 1) {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
"api_key": notification.octopushDMAPIKey,
|
||||
"sms_recipients": notification.octopushDMPhoneNumber,
|
||||
"sms_sender": notification.octopushDMSenderName,
|
||||
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX",
|
||||
"transactional": "1",
|
||||
//octopush not supporting non ascii char
|
||||
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
};
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"cache-control": "no-cache"
|
||||
},
|
||||
params: data
|
||||
};
|
||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config)
|
||||
} else {
|
||||
throw new Error("Unknown Octopush version!");
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
41
server/notification-providers/promosms.js
Normal file
41
server/notification-providers/promosms.js
Normal file
@ -0,0 +1,41 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class PromoSMS extends NotificationProvider {
|
||||
|
||||
name = "promosms";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'),
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
"recipients": [ notification.promosmsPhoneNumber ],
|
||||
//Lets remove non ascii char
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"type": Number(notification.promosmsSMSType),
|
||||
"sender": notification.promosmsSenderName
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config);
|
||||
|
||||
if (resp.data.response.status !== 0) {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.response.status + ".";
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PromoSMS;
|
@ -1,5 +1,8 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const Slack = require("./slack");
|
||||
const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
@ -10,16 +13,17 @@ class RocketChat extends NotificationProvider {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Rocket.chat testing successful.",
|
||||
"text": msg,
|
||||
"channel": notification.rocketchannel,
|
||||
"username": notification.rocketusername,
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
};
|
||||
await axios.post(notification.rocketwebhookURL, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
@ -28,16 +32,32 @@ class RocketChat extends NotificationProvider {
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title_link": notification.rocketbutton,
|
||||
"text": "*Message*\n" + msg,
|
||||
"color": "#32cd32"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Color
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
data.attachments[0].color = "#ff0000";
|
||||
} else {
|
||||
data.attachments[0].color = "#32cd32";
|
||||
}
|
||||
await axios.post(notification.rocketwebhookURL, data)
|
||||
|
||||
if (notification.rocketbutton) {
|
||||
await Slack.deprecateURL(notification.rocketbutton);
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
if (baseURL) {
|
||||
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
await axios.post(notification.rocketwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,27 +1,47 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setSettings, setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
|
||||
name = "slack";
|
||||
|
||||
/**
|
||||
* Deprecated property notification.slackbutton
|
||||
* Set it as primary base url if this is not yet set.
|
||||
*/
|
||||
static async deprecateURL(url) {
|
||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||
|
||||
if (!currentPrimaryBaseURL) {
|
||||
console.log("Move the url to be the primary base URL");
|
||||
await setSettings("general", {
|
||||
primaryBaseURL: url,
|
||||
});
|
||||
} else {
|
||||
console.log("Already there, no need to move the primary base URL");
|
||||
}
|
||||
}
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
"text": "Uptime Kuma Slack testing successful.",
|
||||
"text": msg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
};
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
@ -42,26 +62,35 @@ class Slack extends NotificationProvider {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
}],
|
||||
},
|
||||
{
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.slackbutton) {
|
||||
await Slack.deprecateURL(notification.slackbutton);
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
// Button
|
||||
if (baseURL) {
|
||||
data.blocks.push({
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"elements": [{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||
},
|
||||
],
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
}],
|
||||
});
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error)
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
|
||||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
// Lets start with default subject and empty string for custom one
|
||||
let subject = msg;
|
||||
|
||||
// Change the subject if:
|
||||
// - The msg ends with "Testing" or
|
||||
// - Actual Up/Down Notification
|
||||
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||
let customSubject = "";
|
||||
|
||||
// Our subject cannot end with whitespace it's often raise spam score
|
||||
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
|
||||
if (notification.customSubject) {
|
||||
customSubject = notification.customSubject.trim();
|
||||
}
|
||||
|
||||
// If custom subject is not empty, change subject for notification
|
||||
if (customSubject !== "") {
|
||||
|
||||
// Replace "MACROS" with corresponding variable
|
||||
let replaceName = new RegExp("{{NAME}}", "g");
|
||||
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
|
||||
let replaceStatus = new RegExp("{{STATUS}}", "g");
|
||||
|
||||
// Lets start with dummy values to simplify code
|
||||
let monitorName = "Test";
|
||||
let monitorHostnameOrURL = "testing.hostname";
|
||||
let serviceStatus = "⚠️ Test";
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeatJSON !== null) {
|
||||
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||
}
|
||||
|
||||
// Break replace to one by line for better readability
|
||||
customSubject = customSubject.replace(replaceStatus, serviceStatus);
|
||||
customSubject = customSubject.replace(replaceName, monitorName);
|
||||
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
|
||||
|
||||
subject = customSubject;
|
||||
}
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
|
||||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
|
@ -5,7 +5,9 @@ const Gotify = require("./notification-providers/gotify");
|
||||
const Line = require("./notification-providers/line");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const Pushover = require("./notification-providers/pushover");
|
||||
const Pushy = require("./notification-providers/pushy");
|
||||
@ -16,6 +18,9 @@ const SMTP = require("./notification-providers/smtp");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
|
||||
class Notification {
|
||||
|
||||
@ -28,13 +33,18 @@ class Notification {
|
||||
|
||||
const list = [
|
||||
new Apprise(),
|
||||
new AliyunSms(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Teams(),
|
||||
new Gotify(),
|
||||
new Line(),
|
||||
new LunaSea(),
|
||||
new Feishu(),
|
||||
new Mattermost(),
|
||||
new Matrix(),
|
||||
new Octopush(),
|
||||
new PromoSMS(),
|
||||
new Pushbullet(),
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
|
@ -4,10 +4,7 @@ const net = require("net");
|
||||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const WIN = /^win/.test(process.platform);
|
||||
const LIN = /^linux/.test(process.platform);
|
||||
const MAC = /^darwin/.test(process.platform);
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
@ -23,12 +20,12 @@ function Ping(host, options) {
|
||||
|
||||
const timeout = 10;
|
||||
|
||||
if (WIN) {
|
||||
if (util.WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
|
||||
} else if (LIN) {
|
||||
} else if (util.LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
@ -40,7 +37,7 @@ function Ping(host, options) {
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (MAC) {
|
||||
} else if (util.MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
this._bin = "/sbin/ping6";
|
||||
@ -51,7 +48,7 @@ function Ping(host, options) {
|
||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
} else if (util.FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
|
||||
});
|
||||
|
||||
this._ping.stdout.on("data", function (data) { // log stdout
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
||||
});
|
||||
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
||||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||
* Thank @pemassi
|
||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
if (util.WIN) {
|
||||
if (data) {
|
||||
return util.convertToUTF8(data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ const commonLabels = [
|
||||
"monitor_url",
|
||||
"monitor_hostname",
|
||||
"monitor_port",
|
||||
]
|
||||
];
|
||||
|
||||
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
|
||||
name: "monitor_cert_days_remaining",
|
||||
@ -41,45 +41,46 @@ class Prometheus {
|
||||
monitor_url: monitor.url,
|
||||
monitor_hostname: monitor.hostname,
|
||||
monitor_port: monitor.port
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update(heartbeat, tlsInfo) {
|
||||
|
||||
if (typeof tlsInfo !== "undefined") {
|
||||
try {
|
||||
let is_valid = 0
|
||||
let is_valid = 0;
|
||||
if (tlsInfo.valid == true) {
|
||||
is_valid = 1
|
||||
is_valid = 1;
|
||||
} else {
|
||||
is_valid = 0
|
||||
is_valid = 0;
|
||||
}
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid)
|
||||
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
|
||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status)
|
||||
monitor_status.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
|
||||
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitor_response_time.set(this.monitorLabelValues, -1)
|
||||
monitor_response_time.set(this.monitorLabelValues, -1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,4 +88,4 @@ class Prometheus {
|
||||
|
||||
module.exports = {
|
||||
Prometheus
|
||||
}
|
||||
};
|
||||
|
@ -4,15 +4,89 @@ const { R } = require("redbean-node");
|
||||
const server = require("../server");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
let io = server.io;
|
||||
|
||||
router.get("/api/entry-page", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json(server.entryPage);
|
||||
});
|
||||
|
||||
router.get("/api/push/:pushToken", async (request, response) => {
|
||||
try {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping || null;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
]);
|
||||
|
||||
if (! monitor) {
|
||||
throw new Error("Monitor not found or not active.");
|
||||
}
|
||||
|
||||
const previousHeartbeat = await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitor.id
|
||||
]);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
status = flipStatus(status);
|
||||
}
|
||||
|
||||
let isFirstBeat = true;
|
||||
let previousStatus = status;
|
||||
let duration = 0;
|
||||
|
||||
let bean = R.dispense("heartbeat");
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
|
||||
if (previousHeartbeat) {
|
||||
isFirstBeat = false;
|
||||
previousStatus = previousHeartbeat.status;
|
||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||
}
|
||||
|
||||
debug("PreviousStatus: " + previousStatus);
|
||||
debug("Current Status: " + status);
|
||||
|
||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
||||
bean.monitor_id = monitor.id;
|
||||
bean.status = status;
|
||||
bean.msg = msg;
|
||||
bean.ping = ping;
|
||||
bean.duration = duration;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
if (bean.important) {
|
||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
response.json({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page Config
|
||||
router.get("/api/status-page/config", async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
@ -1,4 +1,9 @@
|
||||
console.log("Welcome to Uptime Kuma");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
debug(args);
|
||||
|
||||
if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
@ -6,8 +11,6 @@ if (! process.env.NODE_ENV) {
|
||||
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
|
||||
|
||||
console.log("Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
@ -37,7 +40,7 @@ console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, genSecret, allowDevAllOrigin, checkLogin } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@ -46,32 +49,52 @@ Notification.init();
|
||||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
console.info("Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
if (!hostname && !FBSD) {
|
||||
hostname = process.env.HOST;
|
||||
}
|
||||
|
||||
if (hostname) {
|
||||
console.log("Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
|
||||
|
||||
// SSL
|
||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
|
||||
// Demo Mode?
|
||||
const demoMode = args["demo"] || false;
|
||||
// 2FA / notp verification defaults
|
||||
const twofa_verification_opts = {
|
||||
"window": 1,
|
||||
"time": 30
|
||||
}
|
||||
|
||||
if (demoMode) {
|
||||
/**
|
||||
* Run unit test after the server is ready
|
||||
* @type {boolean}
|
||||
*/
|
||||
const testMode = !!args["test"] || false;
|
||||
|
||||
if (config.demoMode) {
|
||||
console.log("==== Demo Mode ====");
|
||||
}
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
console.log("Creating express and socket.io instance");
|
||||
const app = express();
|
||||
|
||||
let server;
|
||||
@ -91,7 +114,7 @@ const io = new Server(server);
|
||||
module.exports.io = io;
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
@ -181,10 +204,7 @@ exports.entryPage = "dashboard";
|
||||
console.log("Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
});
|
||||
sendInfo(socket);
|
||||
|
||||
totalClient++;
|
||||
|
||||
@ -261,7 +281,7 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret);
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (verify && verify.delta == 0) {
|
||||
callback({
|
||||
@ -303,6 +323,12 @@ exports.entryPage = "dashboard";
|
||||
if (user.twofa_status == 0) {
|
||||
let newSecret = await genSecret();
|
||||
let encodedSecret = base32.encode(newSecret);
|
||||
|
||||
// Google authenticator doesn't like equal signs
|
||||
// The fix is found at https://github.com/guyht/notp
|
||||
// Related issue: https://github.com/louislam/uptime-kuma/issues/486
|
||||
encodedSecret = encodedSecret.toString().replace(/=/g, "");
|
||||
|
||||
let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
|
||||
@ -373,7 +399,7 @@ exports.entryPage = "dashboard";
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
let verify = notp.totp.verify(token, user.twofa_secret);
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (verify && verify.delta == 0) {
|
||||
callback({
|
||||
@ -499,6 +525,9 @@ exports.entryPage = "dashboard";
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
@ -512,6 +541,7 @@ exports.entryPage = "dashboard";
|
||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
bean.pushToken = monitor.pushToken;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
@ -639,6 +669,8 @@ exports.entryPage = "dashboard";
|
||||
});
|
||||
|
||||
await sendMonitorList(socket);
|
||||
// Clear heartbeat list on client
|
||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@ -863,6 +895,8 @@ exports.entryPage = "dashboard";
|
||||
msg: "Saved"
|
||||
});
|
||||
|
||||
sendInfo(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
@ -1020,6 +1054,9 @@ exports.entryPage = "dashboard";
|
||||
name: monitorListData[i].name,
|
||||
type: monitorListData[i].type,
|
||||
url: monitorListData[i].url,
|
||||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
hostname: monitorListData[i].hostname,
|
||||
@ -1035,6 +1072,10 @@ exports.entryPage = "dashboard";
|
||||
notificationIDList: {},
|
||||
};
|
||||
|
||||
if (monitorListData[i].pushToken) {
|
||||
monitor.pushToken = monitorListData[i].pushToken;
|
||||
}
|
||||
|
||||
let bean = R.dispense("monitor");
|
||||
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
@ -1215,8 +1256,14 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
|
||||
if (testMode) {
|
||||
startUnitTest();
|
||||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
|
@ -5,6 +5,15 @@ const { debug } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
exports.LIN = /^linux/.test(process.platform);
|
||||
exports.MAC = /^darwin/.test(process.platform);
|
||||
exports.FBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
@ -115,7 +124,7 @@ exports.setting = async function (key) {
|
||||
}
|
||||
};
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
exports.setSetting = async function (key, value, type = null) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
]);
|
||||
@ -123,6 +132,7 @@ exports.setSetting = async function (key, value) {
|
||||
bean = R.dispense("setting");
|
||||
bean.key = key;
|
||||
}
|
||||
bean.type = type;
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean);
|
||||
};
|
||||
@ -185,38 +195,42 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const {
|
||||
valid_from,
|
||||
valid_to,
|
||||
subjectaltname,
|
||||
issuer,
|
||||
fingerprint,
|
||||
} = res.request.res.socket.getPeerCertificate(false);
|
||||
// Fix certificate Info for display
|
||||
// param: info - the chain obtained from getPeerCertificate()
|
||||
const parseCertificateInfo = function (info) {
|
||||
let link = info;
|
||||
|
||||
if (!valid_from || !valid_to || !subjectaltname) {
|
||||
throw {
|
||||
message: "No TLS certificate in response",
|
||||
};
|
||||
while (link) {
|
||||
if (!link.valid_from || !link.valid_to) {
|
||||
break;
|
||||
}
|
||||
link.validTo = new Date(link.valid_to);
|
||||
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
|
||||
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
|
||||
|
||||
// Move up the chain until loop is encountered
|
||||
if (link.issuerCertificate == null) {
|
||||
break;
|
||||
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
link = link.issuerCertificate;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
const validTo = new Date(valid_to);
|
||||
|
||||
const validFor = subjectaltname
|
||||
.replace(/DNS:|IP Address:/g, "")
|
||||
.split(", ");
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
return {
|
||||
valid,
|
||||
validFor,
|
||||
validTo,
|
||||
daysRemaining,
|
||||
issuer,
|
||||
fingerprint,
|
||||
valid: valid,
|
||||
certInfo: parsedInfo
|
||||
};
|
||||
};
|
||||
|
||||
@ -272,16 +286,6 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.genSecret = () => {
|
||||
let secret = "";
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let charsLength = chars.length;
|
||||
for ( let i = 0; i < 64; i++ ) {
|
||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
return secret;
|
||||
};
|
||||
|
||||
exports.allowDevAllOrigin = (res) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
exports.allowAllOrigin(res);
|
||||
@ -298,3 +302,33 @@ exports.checkLogin = (socket) => {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
};
|
||||
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = child_process.spawn(npm, ["run", "jest"]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.on("close", function (code) {
|
||||
console.log("Jest exit code: " + code);
|
||||
process.exit(code);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param body : Buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.convertToUTF8 = (body) => {
|
||||
const guessEncoding = chardet.detect(body);
|
||||
//debug("Guess Encoding: " + guessEncoding);
|
||||
const str = iconv.decode(body, guessEncoding);
|
||||
return str.toString();
|
||||
};
|
||||
|
@ -3,5 +3,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
import { setPageLocale } from "./util-frontend";
|
||||
export default {
|
||||
created() {
|
||||
setPageLocale();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,8 +1,9 @@
|
||||
@import "vars.scss";
|
||||
@import "multiselect.scss";
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
font-family: BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -13,6 +14,10 @@ h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
border-radius: 19px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
@ -179,6 +184,11 @@ h2 {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.form-control:disabled, .form-control[readonly] {
|
||||
background-color: #232f3b;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
--bs-table-accent-bg: #070a10;
|
||||
color: $dark-font-color;
|
||||
@ -233,30 +243,6 @@ h2 {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
// Multiselect
|
||||
.multiselect__tags {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__input, .multiselect__single {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.item {
|
||||
&:hover {
|
||||
@ -428,3 +414,7 @@ h2 {
|
||||
.vue-image-crop-upload .vicp-wrap {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
5
src/assets/localization.scss
Normal file
5
src/assets/localization.scss
Normal file
@ -0,0 +1,5 @@
|
||||
html[lang='fa'] {
|
||||
#app {
|
||||
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
}
|
73
src/assets/multiselect.scss
Normal file
73
src/assets/multiselect.scss
Normal file
@ -0,0 +1,73 @@
|
||||
@import "vars.scss";
|
||||
@import "node_modules/vue-multiselect/dist/vue-multiselect";
|
||||
|
||||
.multiselect__tags {
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
min-height: 38px;
|
||||
padding: 6px 40px 0 8px;
|
||||
}
|
||||
|
||||
.multiselect--active .multiselect__tags {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight {
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight::after {
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: 0;
|
||||
padding: 6px 26px 6px 10px;
|
||||
background: $primary !important;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
font-size: 1rem;
|
||||
padding-left: 6px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.67;
|
||||
}
|
||||
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
line-height: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.multiselect__tag {
|
||||
color: $dark-font-color2;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
background-color: $dark-bg2;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
|
||||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
52
src/components/CertificateInfo.vue
Normal file
52
src/components/CertificateInfo.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4>{{ $t("Certificate Info") }}</h4>
|
||||
{{ $t("Certificate Chain") }}:
|
||||
<div
|
||||
v-if="valid"
|
||||
class="rounded d-inline-flex ms-2 text-white tag-valid"
|
||||
>
|
||||
{{ $t("Valid") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!valid"
|
||||
class="rounded d-inline-flex ms-2 text-white tag-invalid"
|
||||
>
|
||||
{{ $t("Invalid") }}
|
||||
</div>
|
||||
<certificate-info-row :cert="certInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CertificateInfoRow from "./CertificateInfoRow.vue";
|
||||
export default {
|
||||
components: {
|
||||
CertificateInfoRow,
|
||||
},
|
||||
props: {
|
||||
certInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
valid: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.tag-valid {
|
||||
padding: 2px 25px;
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
.tag-invalid {
|
||||
padding: 2px 25px;
|
||||
background-color: $danger;
|
||||
}
|
||||
</style>
|
122
src/components/CertificateInfoRow.vue
Normal file
122
src/components/CertificateInfoRow.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex flex-row align-items-center p-1 overflow-hidden">
|
||||
<div class="m-3 ps-3">
|
||||
<div class="cert-icon">
|
||||
<font-awesome-icon icon="file" />
|
||||
<font-awesome-icon class="award-icon" icon="award" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<table class="text-start">
|
||||
<tbody>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Subject:</td>
|
||||
<td>{{ formatSubject(cert.subject) }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Valid To:</td>
|
||||
<td><Datetime :value="cert.validTo" /></td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Days Remaining:</td>
|
||||
<td>{{ cert.daysRemaining }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Issuer:</td>
|
||||
<td>{{ formatSubject(cert.issuer) }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Fingerprint:</td>
|
||||
<td>{{ cert.fingerprint }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<font-awesome-icon
|
||||
v-if="cert.issuerCertificate"
|
||||
class="m-2 ps-6 link-icon"
|
||||
icon="link"
|
||||
/>
|
||||
</div>
|
||||
<certificate-info-row
|
||||
v-if="cert.issuerCertificate"
|
||||
:cert="cert.issuerCertificate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
export default {
|
||||
name: "CertificateInfoRow",
|
||||
components: {
|
||||
Datetime,
|
||||
},
|
||||
props: {
|
||||
cert: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatSubject(subject) {
|
||||
if (subject.O && subject.CN && subject.C) {
|
||||
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||
} else if (subject.O && subject.CN) {
|
||||
return `${subject.CN} - ${subject.O}`;
|
||||
} else if (subject.CN) {
|
||||
return subject.CN;
|
||||
} else {
|
||||
return "no info";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cert-icon {
|
||||
position: relative;
|
||||
font-size: 70px;
|
||||
color: $link-color;
|
||||
opacity: 0.5;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.award-icon {
|
||||
position: absolute;
|
||||
font-size: 0.5em;
|
||||
bottom: 20%;
|
||||
left: 12%;
|
||||
color: white;
|
||||
|
||||
.dark & {
|
||||
color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 20px;
|
||||
margin-left: 50px !important;
|
||||
color: $link-color;
|
||||
opacity: 0.5;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
</style>
|
122
src/components/CopyableInput.vue
Normal file
122
src/components/CopyableInput.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="input-group">
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
v-model="model"
|
||||
:type="type"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocomplete"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
||||
<a class="btn btn-outline-primary" @click="copyToClipboard(model)">
|
||||
<font-awesome-icon :icon="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
let timeout;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text"
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
required: {
|
||||
type: Boolean
|
||||
},
|
||||
readonly: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visibility: "password",
|
||||
icon: "copy",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
showInput() {
|
||||
this.visibility = "text";
|
||||
},
|
||||
|
||||
hideInput() {
|
||||
this.visibility = "password";
|
||||
},
|
||||
|
||||
copyToClipboard(textToCopy) {
|
||||
this.icon = "check";
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
this.icon = "copy";
|
||||
}, 3000);
|
||||
|
||||
// navigator clipboard api needs a secure context (https)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard api method'
|
||||
return navigator.clipboard.writeText(textToCopy);
|
||||
} else {
|
||||
// text area method
|
||||
let textArea = document.createElement("textarea");
|
||||
textArea.value = textToCopy;
|
||||
// make the textarea out of viewport
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
textArea.style.top = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
return new Promise((res, rej) => {
|
||||
// here the magic happens
|
||||
document.execCommand("copy") ? res() : rej();
|
||||
textArea.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
@ -186,7 +186,7 @@ export default {
|
||||
.beat {
|
||||
display: inline-block;
|
||||
background-color: $primary;
|
||||
border-radius: 50rem;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
|
@ -52,7 +52,7 @@ export default {
|
||||
token: "",
|
||||
res: null,
|
||||
tokenRequired: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
@ -60,21 +60,19 @@ export default {
|
||||
|
||||
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||
this.processing = false;
|
||||
console.log(res)
|
||||
|
||||
if (res.tokenRequired) {
|
||||
this.tokenRequired = true;
|
||||
} else {
|
||||
this.res = res;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -82,8 +80,17 @@ export default {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.form {
|
||||
.form-floating {
|
||||
> label {
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
|
||||
> .form-control {
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
@ -28,7 +28,7 @@
|
||||
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
@ -47,6 +47,7 @@
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -62,7 +63,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
@ -91,7 +92,7 @@ export default {
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
});
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
@ -100,8 +101,8 @@ export default {
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText))
|
||||
})
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -109,13 +110,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -13,23 +13,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="notification-type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="teams">Microsoft Teams</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="rocket.chat">Rocket.chat</option>
|
||||
<option value="pushover">Pushover</option>
|
||||
<option value="pushy">Pushy</option>
|
||||
<option value="octopush">Octopush</option>
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
<option value="line">Line Messenger</option>
|
||||
<option value="mattermost">Mattermost</option>
|
||||
<option v-for="type in notificationTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -38,370 +22,8 @@
|
||||
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<Telegram v-if="notification.type === 'telegram'" />
|
||||
|
||||
<!-- TODO: Convert all into vue components, but not an easy task. -->
|
||||
|
||||
<template v-if="notification.type === 'webhook'">
|
||||
<div class="mb-3">
|
||||
<label for="webhook-url" class="form-label">Post URL</label>
|
||||
<input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||
<select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
|
||||
<option value="json">
|
||||
application/json
|
||||
</option>
|
||||
<option value="form-data">
|
||||
multipart/form-data
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>"application/json" is good for any modern http servers such as express.js</p>
|
||||
<p>"multipart/form-data" is good for PHP, you just need to parse the json by <strong>json_decode($_POST['data'])</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<SMTP v-if="notification.type === 'smtp'" />
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||
<input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
|
||||
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'signal'">
|
||||
<div class="mb-3">
|
||||
<label for="signal-url" class="form-label">Post URL</label>
|
||||
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">Number</label>
|
||||
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
You need to have a signal client with REST API.
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can check this url to view how to setup one:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
IMPORTANT: You cannot mix groups and numbers in recipients!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'gotify'">
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">Priority</label>
|
||||
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
|
||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'rocket.chat'">
|
||||
<div class="mb-3">
|
||||
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||
<label for="rocket-username" class="form-label">Username</label>
|
||||
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
|
||||
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
|
||||
<label for="rocket-channel" class="form-label">Channel Name</label>
|
||||
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
|
||||
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'mattermost'">
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">Username</label>
|
||||
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">Channel Name</label>
|
||||
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushy'">
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'octopush'">
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-type-sms" class="form-label">SMS Type</label>
|
||||
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
|
||||
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
|
||||
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
|
||||
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
|
||||
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">Priority</label>
|
||||
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you want to send notifications to different devices, fill out Device field.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'apprise'">
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||
<p>
|
||||
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Status:
|
||||
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushbullet'">
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">Access Token</label>
|
||||
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'line'">
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Basic Settings</b>
|
||||
</div>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Messaging API</b>
|
||||
</div>
|
||||
<div class="form-text" style="margin-top: 8px;">
|
||||
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
|
||||
|
||||
<Teams v-if="notification.type === 'teams'" />
|
||||
<!-- form body -->
|
||||
<component :is="currentForm" />
|
||||
|
||||
<div class="mb-3 mt-4">
|
||||
<hr class="dropdown-divider mb-4">
|
||||
@ -446,22 +68,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap"
|
||||
import { ucfirst } from "../util.ts"
|
||||
import { Modal } from "bootstrap";
|
||||
import { ucfirst } from "../util.ts";
|
||||
|
||||
import Confirm from "./Confirm.vue";
|
||||
import HiddenInput from "./HiddenInput.vue";
|
||||
import Telegram from "./notifications/Telegram.vue";
|
||||
import Teams from "./notifications/Teams.vue";
|
||||
import SMTP from "./notifications/SMTP.vue";
|
||||
import NotificationFormList from "./notifications";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
HiddenInput,
|
||||
Telegram,
|
||||
Teams,
|
||||
SMTP,
|
||||
},
|
||||
props: {},
|
||||
emits: ["added"],
|
||||
@ -470,43 +85,48 @@ export default {
|
||||
model: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
notificationTypes: Object.keys(NotificationFormList),
|
||||
notification: {
|
||||
name: "",
|
||||
/** @type { null | keyof NotificationFormList } */
|
||||
type: null,
|
||||
isDefault: false,
|
||||
// Do not set default value here, please scroll to show()
|
||||
}
|
||||
};
|
||||
},
|
||||
appriseInstalled: false,
|
||||
|
||||
computed: {
|
||||
currentForm() {
|
||||
if (!this.notification.type) {
|
||||
return null;
|
||||
}
|
||||
return NotificationFormList[this.notification.type];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"notification.type"(to, from) {
|
||||
let oldName;
|
||||
|
||||
if (from) {
|
||||
oldName = `My ${ucfirst(from)} Alert (1)`;
|
||||
oldName = this.getUniqueDefaultName(from);
|
||||
} else {
|
||||
oldName = "";
|
||||
}
|
||||
|
||||
if (! this.notification.name || this.notification.name === oldName) {
|
||||
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
||||
this.notification.name = this.getUniqueDefaultName(to);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
})
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show()
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
show(notificationID) {
|
||||
@ -525,21 +145,19 @@ export default {
|
||||
name: "",
|
||||
type: null,
|
||||
isDefault: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Set Default value here
|
||||
this.notification.type = "telegram";
|
||||
this.notification.gotifyPriority = 8;
|
||||
this.notification.smtpSecure = false;
|
||||
this.notification.type = this.notificationTypes[0];
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
@ -551,30 +169,45 @@ export default {
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
deleteNotification() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||
this.$root.toastRes(res)
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide()
|
||||
this.modal.hide();
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {keyof NotificationFormList} notificationKey
|
||||
* @return {string}
|
||||
*/
|
||||
getUniqueDefaultName(notificationKey) {
|
||||
let index = 1;
|
||||
let name = "";
|
||||
do {
|
||||
name = this.$t("defaultNotificationName", {
|
||||
notification: this.$t(notificationKey).replace(/\(.+\)/, "").trim(),
|
||||
number: index++
|
||||
});
|
||||
} while (this.$root.notificationList.find(it => it.name === name));
|
||||
return name;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -11,18 +11,18 @@ export default {
|
||||
computed: {
|
||||
color() {
|
||||
if (this.status === 0) {
|
||||
return "danger"
|
||||
return "danger";
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "primary"
|
||||
return "primary";
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "warning"
|
||||
return "warning";
|
||||
}
|
||||
|
||||
return "secondary"
|
||||
return "secondary";
|
||||
},
|
||||
|
||||
text() {
|
||||
@ -41,11 +41,11 @@ export default {
|
||||
return this.$t("Unknown");
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||
<div class="mb-3 p-1">
|
||||
<h4 class="mt-5 mb-3">{{ $t("Tags") }}</h4>
|
||||
<div v-if="selectedTags.length > 0" class="mb-2 p-1">
|
||||
<tag
|
||||
v-for="item in selectedTags"
|
||||
:key="item.id"
|
||||
@ -124,8 +124,8 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { useToast } from "vue-toastification"
|
||||
const toast = useToast()
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -186,7 +186,7 @@ export default {
|
||||
color: "#7C3AED" },
|
||||
{ name: this.$t("Pink"),
|
||||
color: "#DB2777" },
|
||||
]
|
||||
];
|
||||
},
|
||||
validateDraftTag() {
|
||||
let nameInvalid = false;
|
||||
@ -227,7 +227,7 @@ export default {
|
||||
invalid,
|
||||
nameInvalid,
|
||||
valueInvalid,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -243,7 +243,7 @@ export default {
|
||||
if (res.ok) {
|
||||
this.existingTags = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg)
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -277,7 +277,7 @@ export default {
|
||||
name: this.newDraftTag.select.name,
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Add new Tag
|
||||
@ -286,7 +286,7 @@ export default {
|
||||
name: this.newDraftTag.name.trim(),
|
||||
value: this.newDraftTag.value,
|
||||
new: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
this.clearDraftTag();
|
||||
},
|
||||
@ -348,7 +348,7 @@ export default {
|
||||
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||
tag.id = newTagResult.id;
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
tagId = newTag.id;
|
||||
}
|
||||
|
25
src/components/notifications/AliyunSms.vue
Normal file
25
src/components/notifications/AliyunSms.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||
|
||||
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||
|
||||
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
|
||||
|
||||
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
35
src/components/notifications/Apprise.vue
Normal file
35
src/components/notifications/Apprise.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">{{ $t("Apprise URL") }}</label>
|
||||
<input id="apprise-url" v-model="$parent.notification.appriseURL" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p>{{ $t("Example:", ["twilio://AccountSid:AuthToken@FromPhoneNo"]) }}</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<i18n-t tag="p" keypath="Status:">
|
||||
<span v-if="appriseInstalled" class="text-primary">{{ $t("appriseInstalled") }}</span>
|
||||
<i18n-t v-else tag="span" keypath="appriseNotInstalled" class="text-danger">
|
||||
<a href="https://github.com/caronc/apprise" target="_blank">{{ $t("Read more") }}</a>
|
||||
</i18n-t>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
appriseInstalled: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
16
src/components/notifications/DingDing.vue
Normal file
16
src/components/notifications/DingDing.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>For safety, must use secret key</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
19
src/components/notifications/Discord.vue
Normal file
19
src/components/notifications/Discord.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="discord-webhook-url" class="form-label">{{ $t("Discord Webhook URL") }}</label>
|
||||
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||
<div class="form-text">
|
||||
{{ $t("wayToGetDiscordURL") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-username" class="form-label">{{ $t("Bot Display Name") }}</label>
|
||||
<input id="discord-username" v-model="$parent.notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
|
||||
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" :placeholder="$t('Hello @everyone is...')">
|
||||
</div>
|
||||
</template>
|
15
src/components/notifications/Feishu.vue
Normal file
15
src/components/notifications/Feishu.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="Feishu-WebHookUrl" class="form-label">{{ $t("Feishu WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="Feishu-WebHookUrl" v-model="$parent.notification.feishuWebHookUrl" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<a
|
||||
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||
target="_blank"
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
32
src/components/notifications/Gotify.vue
Normal file
32
src/components/notifications/Gotify.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
|
||||
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="gotify-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<input id="gotify-priority" v-model="$parent.notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.gotifyPriority === "undefined") {
|
||||
this.$parent.notification.gotifyPriority = 8;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
29
src/components/notifications/Line.vue
Normal file
29
src/components/notifications/Line.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
|
||||
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Basic Settings") }}</b>
|
||||
</i18n-t>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Messaging API") }}</b>
|
||||
</i18n-t>
|
||||
<i18n-t tag="div" keypath="wayToGetLineChannelToken" class="form-text" style="margin-top: 8px;">
|
||||
<a href="https://developers.line.biz/console/" target="_blank">{{ $t("Line Developers Console") }}</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
9
src/components/notifications/LunaSea.vue
Normal file
9
src/components/notifications/LunaSea.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">{{ $t("LunaSea Device ID") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
34
src/components/notifications/Matrix.vue
Normal file
34
src/components/notifications/Matrix.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="homeserver-url" class="form-label">{{ $t("matrixHomeserverURL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="internal-room-id" class="form-label">{{ $t("Internal Room Id") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("matrixDesc1") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
|
||||
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
32
src/components/notifications/Mattermost.vue
Normal file
32
src/components/notifications/Mattermost.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">{{ $t("Icon URL") }}</label>
|
||||
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
|
||||
<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://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") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutIconURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
50
src/components/notifications/Octopush.vue
Normal file
50
src/components/notifications/Octopush.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-version" class="form-label">Octopush API Version</label>
|
||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||
<option value="2">Octopush (endpoint: api.octopush.com)</option>
|
||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("octopushLegacyHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
||||
<select id="octopush-type-sms" v-model="$parent.notification.octopushSMSType" class="form-select">
|
||||
<option value="sms_premium">{{ $t("octopushTypePremium") }}</option>
|
||||
<option value="sms_low_cost">{{ $t("octopushTypeLowCost") }}</option>
|
||||
</select>
|
||||
<i18n-t tag="div" keypath="Check octopush prices" class="form-text">
|
||||
<a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-phone-number" class="form-label">{{ $t("octopushPhoneNumber") }}</label>
|
||||
<input id="octopush-phone-number" v-model="$parent.notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-sender-name" class="form-label">{{ $t("octopushSMSSender") }}</label>
|
||||
<input id="octopush-sender-name" v-model="$parent.notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
39
src/components/notifications/PromoSMS.vue
Normal file
39
src/components/notifications/PromoSMS.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-login" class="form-label">API LOGIN</label>
|
||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||
<label for="promosms-key" class="form-label">API PASSWORD</label>
|
||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
|
||||
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
|
||||
<option value="0">{{ $t("promosmsTypeFlash") }}</option>
|
||||
<option value="1">{{ $t("promosmsTypeEco") }}</option>
|
||||
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
||||
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("checkPrice", [$t("promosms")]) }}
|
||||
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
||||
<input id="promosms-phone-number" v-model="$parent.notification.promosmsPhoneNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
|
||||
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
20
src/components/notifications/Pushbullet.vue
Normal file
20
src/components/notifications/Pushbullet.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
|
||||
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
67
src/components/notifications/Pushover.vue
Normal file
67
src/components/notifications/Pushover.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
|
||||
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
||||
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
|
||||
<input id="pushover-title" v-model="$parent.notification.pushovertitle" type="text" class="form-control">
|
||||
<label for="pushover-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<select id="pushover-priority" v-model="$parent.notification.pushoverpriority" class="form-select">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
||||
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("pushoverDesc1") }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("pushoverDesc2") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
}
|
||||
</script>
|
26
src/components/notifications/Pushy.vue
Normal file
26
src/components/notifications/Pushy.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></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>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
27
src/components/notifications/RocketChat.vue
Normal file
27
src/components/notifications/RocketChat.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="rocket-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="rocket-webhook-url" v-model="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||
<label for="rocket-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
|
||||
<label for="rocket-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
|
||||
<label for="rocket-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="rocket-channel-name" v-model="$parent.notification.rocketchannel" type="text" class="form-control">
|
||||
<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://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -12,8 +12,8 @@
|
||||
<div class="mb-3">
|
||||
<label for="secure" class="form-label">Secure</label>
|
||||
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||
<option :value="false">None / STARTTLS (25, 587)</option>
|
||||
<option :value="true">TLS (465)</option>
|
||||
<option :value="false">{{ $t("secureOptionNone") }}</option>
|
||||
<option :value="true">{{ $t("secureOptionTLS") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<div class="form-check">
|
||||
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls-error">
|
||||
Ignore TLS Error
|
||||
{{ $t("Ignore TLS Error") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,25 +37,37 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="from-email" class="form-label">From Email</label>
|
||||
<label for="from-email" class="form-label">{{ $t("From Email") }}</label>
|
||||
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-email" class="form-label">To Email</label>
|
||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
|
||||
<label for="to-email" class="form-label">{{ $t("To Email") }}</label>
|
||||
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-cc" class="form-label">CC</label>
|
||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
|
||||
<label for="to-cc" class="form-label">{{ $t("smtpCC") }}</label>
|
||||
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="to-bcc" class="form-label">BCC</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
|
||||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div v-pre class="form-text">
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{URL}}: URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -66,10 +78,19 @@ export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "smtp",
|
||||
computed: {
|
||||
hasRecipient() {
|
||||
if (this.$parent.notification.smtpTo || this.$parent.notification.smtpCC || this.$parent.notification.smtpBCC) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.smtpSecure === "undefined") {
|
||||
this.$parent.notification.smtpSecure = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
34
src/components/notifications/Signal.vue
Normal file
34
src/components/notifications/Signal.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="signal-url" class="form-label">{{ $t("Post URL") }}</label>
|
||||
<input id="signal-url" v-model="$parent.notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-number" class="form-label">{{ $t("Number") }}</label>
|
||||
<input id="signal-number" v-model="$parent.notification.signalNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">{{ $t("Recipients") }}</label>
|
||||
<input id="signal-recipients" v-model="$parent.notification.signalRecipients" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("needSignalAPI") }}
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("wayToCheckSignalURL") }}
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("signalImportant") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
28
src/components/notifications/Slack.vue
Normal file
28
src/components/notifications/Slack.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
||||
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
||||
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
||||
|
||||
<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://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</i18n-t>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutChannelName", [$t("slack")]) }}
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
{{ $t("aboutKumaURL") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
|
||||
<label for="teams-webhookurl" class="form-label">{{ $t("Webhook URL") }}</label>
|
||||
<input
|
||||
id="teams-webhookurl"
|
||||
v-model="$parent.notification.webhookUrl"
|
||||
@ -8,22 +8,11 @@
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<div class="form-text">
|
||||
You can learn how to create a webhook url
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<a
|
||||
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
|
||||
target="_blank"
|
||||
>here</a>.
|
||||
</div>
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: "teams",
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
||||
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<div class="form-text">
|
||||
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
||||
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
<label for="telegram-chat-id" class="form-label">{{ $t("Chat ID") }}</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required>
|
||||
@ -18,10 +18,10 @@
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
{{ $t("supportTelegramChatID") }}
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
{{ $t("wayToGetTelegramChatID") }}
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
@ -47,14 +47,9 @@ export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "telegram",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
telegramGetUpdatesURL() {
|
||||
let token = "<YOUR BOT TOKEN HERE>"
|
||||
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`
|
||||
|
||||
if (this.$parent.notification.telegramBotToken) {
|
||||
token = this.$parent.notification.telegramBotToken;
|
||||
@ -62,9 +57,6 @@ export default {
|
||||
|
||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
async autoGetTelegramChatID() {
|
||||
@ -79,11 +71,11 @@ export default {
|
||||
} else if (update.message) {
|
||||
this.notification.telegramChatID = update.message.chat.id;
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
throw new Error(this.$t("chatIDNotFound"))
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Chat ID is not found, please send a message to this bot first")
|
||||
throw new Error(this.$t("chatIDNotFound"))
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
28
src/components/notifications/Webhook.vue
Normal file
28
src/components/notifications/Webhook.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
|
||||
<input id="webhook-url" v-model="$parent.notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">{{ $t("Content Type") }}</label>
|
||||
<select id="webhook-content-type" v-model="$parent.notification.webhookContentType" class="form-select" required>
|
||||
<option value="json">
|
||||
application/json
|
||||
</option>
|
||||
<option value="form-data">
|
||||
multipart/form-data
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
|
||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||
<template #multipart>"multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
<strong>json_decode($_POST['data'])</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
54
src/components/notifications/index.js
Normal file
54
src/components/notifications/index.js
Normal file
@ -0,0 +1,54 @@
|
||||
import STMP from "./SMTP.vue"
|
||||
import Telegram from "./Telegram.vue";
|
||||
import Discord from "./Discord.vue";
|
||||
import Webhook from "./Webhook.vue";
|
||||
import Signal from "./Signal.vue";
|
||||
import Gotify from "./Gotify.vue";
|
||||
import Slack from "./Slack.vue";
|
||||
import RocketChat from "./RocketChat.vue";
|
||||
import Teams from "./Teams.vue";
|
||||
import Pushover from "./Pushover.vue";
|
||||
import Pushy from "./Pushy.vue";
|
||||
import Octopush from "./Octopush.vue";
|
||||
import PromoSMS from "./PromoSMS.vue";
|
||||
import LunaSea from "./LunaSea.vue";
|
||||
import Feishu from "./Feishu.vue";
|
||||
import Apprise from "./Apprise.vue";
|
||||
import Pushbullet from "./Pushbullet.vue";
|
||||
import Line from "./Line.vue";
|
||||
import Mattermost from "./Mattermost.vue";
|
||||
import Matrix from "./Matrix.vue";
|
||||
import AliyunSMS from "./AliyunSms.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
*
|
||||
* @type { Record<string, any> }
|
||||
*/
|
||||
const NotificationFormList = {
|
||||
"telegram": Telegram,
|
||||
"webhook": Webhook,
|
||||
"smtp": STMP,
|
||||
"discord": Discord,
|
||||
"teams": Teams,
|
||||
"signal": Signal,
|
||||
"gotify": Gotify,
|
||||
"slack": Slack,
|
||||
"rocket.chat": RocketChat,
|
||||
"pushover": Pushover,
|
||||
"pushy": Pushy,
|
||||
"octopush": Octopush,
|
||||
"promosms": PromoSMS,
|
||||
"lunasea": LunaSea,
|
||||
"Feishu": Feishu,
|
||||
"AliyunSMS": AliyunSMS,
|
||||
"apprise": Apprise,
|
||||
"pushbullet": Pushbullet,
|
||||
"line": Line,
|
||||
"mattermost": Mattermost,
|
||||
"matrix": Matrix,
|
||||
"DingDing": DingDing
|
||||
}
|
||||
|
||||
export default NotificationFormList
|
29
src/i18n.js
29
src/i18n.js
@ -1,33 +1,43 @@
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { createI18n } from "vue-i18n/index";
|
||||
import daDK from "./languages/da-DK";
|
||||
import deDE from "./languages/de-DE";
|
||||
import en from "./languages/en";
|
||||
import esEs from "./languages/es-ES";
|
||||
import ptBR from "./languages/pt-BR";
|
||||
import etEE from "./languages/et-EE";
|
||||
import fa from "./languages/fa";
|
||||
import frFR from "./languages/fr-FR";
|
||||
import hu from "./languages/hu";
|
||||
import itIT from "./languages/it-IT";
|
||||
import idID from "./languages/id-ID";
|
||||
import ja from "./languages/ja";
|
||||
import koKR from "./languages/ko-KR";
|
||||
import nlNL from "./languages/nl-NL";
|
||||
import nbNO from "./languages/nb-NO";
|
||||
import pl from "./languages/pl";
|
||||
import ptBR from "./languages/pt-BR";
|
||||
import bgBG from "./languages/bg-BG";
|
||||
import ruRU from "./languages/ru-RU";
|
||||
import sr from "./languages/sr";
|
||||
import srLatn from "./languages/sr-latn";
|
||||
import trTR from "./languages/tr-TR";
|
||||
import svSE from "./languages/sv-SE";
|
||||
import trTR from "./languages/tr-TR";
|
||||
import zhCN from "./languages/zh-CN";
|
||||
import zhHK from "./languages/zh-HK";
|
||||
|
||||
const languageList = {
|
||||
en,
|
||||
"zh-HK": zhHK,
|
||||
"bg-BG": bgBG,
|
||||
"de-DE": deDE,
|
||||
"nl-NL": nlNL,
|
||||
"nb-NO": nbNO,
|
||||
"es-ES": esEs,
|
||||
"fa": fa,
|
||||
"pt-BR": ptBR,
|
||||
"fr-FR": frFR,
|
||||
"hu": hu,
|
||||
"it-IT": itIT,
|
||||
"id-ID" : idID,
|
||||
"ja": ja,
|
||||
"da-DK": daDK,
|
||||
"sr": sr,
|
||||
@ -41,8 +51,19 @@ const languageList = {
|
||||
"et-EE": etEE,
|
||||
};
|
||||
|
||||
const rtlLangs = ["fa"];
|
||||
|
||||
export const currentLocale = () => localStorage.locale
|
||||
|| languageList[navigator.language] && navigator.language
|
||||
|| languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
|
||||
|| "en";
|
||||
|
||||
export const localeDirection = () => {
|
||||
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";
|
||||
};
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: localStorage.locale || "en",
|
||||
locale: currentLocale(),
|
||||
fallbackLocale: "en",
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
|
13
src/icon.js
13
src/icon.js
@ -26,7 +26,13 @@ import {
|
||||
faArrowsAltV,
|
||||
faUnlink,
|
||||
faQuestionCircle,
|
||||
faImages, faUpload,
|
||||
faImages,
|
||||
faUpload,
|
||||
faCopy,
|
||||
faCheck,
|
||||
faFile,
|
||||
faAward,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@ -54,6 +60,11 @@ library.add(
|
||||
faQuestionCircle,
|
||||
faImages,
|
||||
faUpload,
|
||||
faCopy,
|
||||
faCheck,
|
||||
faFile,
|
||||
faAward,
|
||||
faLink,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
200
src/languages/bg-BG.js
Normal file
200
src/languages/bg-BG.js
Normal file
@ -0,0 +1,200 @@
|
||||
export default {
|
||||
languageName: "Български",
|
||||
checkEverySecond: "Ще се извършва на всеки {0} секунди",
|
||||
retryCheckEverySecond: "Ще се извършва на всеки {0} секунди",
|
||||
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие",
|
||||
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
|
||||
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.",
|
||||
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
|
||||
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
|
||||
passwordNotMatchMsg: "Повторената парола не съвпада.",
|
||||
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.",
|
||||
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
|
||||
pauseDashboardHome: "Пауза",
|
||||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
||||
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
|
||||
resoverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
|
||||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
||||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
||||
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
|
||||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
||||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
||||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
||||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известяване със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известяване.",
|
||||
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
||||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
||||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
||||
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?",
|
||||
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?",
|
||||
Settings: "Настройки",
|
||||
Dashboard: "Табло",
|
||||
"New Update": "Налична е актуализация",
|
||||
Language: "Език",
|
||||
Appearance: "Изглед",
|
||||
Theme: "Тема",
|
||||
General: "Общи",
|
||||
Version: "Версия",
|
||||
"Check Update On GitHub": "Проверка за актуализация в GitHub",
|
||||
List: "Списък",
|
||||
Add: "Добави",
|
||||
"Add New Monitor": "Добави монитор",
|
||||
"Quick Stats": "Кратка статистика",
|
||||
Up: "Достъпен",
|
||||
Down: "Недостъпен",
|
||||
Pending: "Изчаква",
|
||||
Unknown: "Неизвестен",
|
||||
Pause: "Пауза",
|
||||
Name: "Име",
|
||||
Status: "Статус",
|
||||
DateTime: "Дата и час",
|
||||
Message: "Отговор",
|
||||
"No important events": "Няма важни събития",
|
||||
Resume: "Възобнови",
|
||||
Edit: "Редактирай",
|
||||
Delete: "Изтрий",
|
||||
Current: "Текущ",
|
||||
Uptime: "Достъпност",
|
||||
"Cert Exp.": "Вал. сертификат",
|
||||
days: "дни",
|
||||
day: "ден",
|
||||
"-day": "-дни",
|
||||
hour: "час",
|
||||
"-hour": "-часa",
|
||||
Response: "Отговор",
|
||||
Ping: "Пинг",
|
||||
"Monitor Type": "Монитор тип",
|
||||
Keyword: "Ключова дума",
|
||||
"Friendly Name": "Псевдоним",
|
||||
URL: "URL Адрес",
|
||||
Hostname: "Име на хост",
|
||||
Port: "Порт",
|
||||
"Heartbeat Interval": "Честота на проверка",
|
||||
Retries: "Повторни опити",
|
||||
"Heartbeat Retry Interval": "Честота на повторните опити",
|
||||
Advanced: "Разширени",
|
||||
"Upside Down Mode": "Обърнат режим",
|
||||
"Max. Redirects": "Макс. брой пренасочвания",
|
||||
"Accepted Status Codes": "Допустими статус кодове",
|
||||
Save: "Запази",
|
||||
Notifications: "Известявания",
|
||||
"Not available, please setup.": "Не е налично. Моля, настройте.",
|
||||
"Setup Notification": "Настройки за известявания",
|
||||
Light: "Светла",
|
||||
Dark: "Тъмна",
|
||||
Auto: "Автоматично",
|
||||
"Theme - Heartbeat Bar": "Тема - поле проверки",
|
||||
Normal: "Нормално",
|
||||
Bottom: "Долу",
|
||||
None: "Без",
|
||||
Timezone: "Часова зона",
|
||||
"Search Engine Visibility": "Видимост за търсачки",
|
||||
"Allow indexing": "Разреши индексиране",
|
||||
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките",
|
||||
"Change Password": "Промени парола",
|
||||
"Current Password": "Текуща парола",
|
||||
"New Password": "Нова парола",
|
||||
"Repeat New Password": "Повторете новата парола",
|
||||
"Update Password": "Актуализирай парола",
|
||||
"Disable Auth": "Изключи удостоверяване",
|
||||
"Enable Auth": "Включи удостоверяване",
|
||||
Logout: "Изход от профила",
|
||||
Leave: "Отказ",
|
||||
"I understand, please disable": "Разбирам. Моля, изключи",
|
||||
Confirm: "Потвърдете",
|
||||
Yes: "Да",
|
||||
No: "Не",
|
||||
Username: "Потребител",
|
||||
Password: "Парола",
|
||||
"Remember me": "Запомни ме",
|
||||
Login: "Вход",
|
||||
"No Monitors, please": "Моля, без монитори",
|
||||
"add one": "добави един",
|
||||
"Notification Type": "Тип известяване",
|
||||
Email: "Имейл",
|
||||
Test: "Тест",
|
||||
"Certificate Info": "Информация за сертификат",
|
||||
"Resolver Server": "Преобразуващ (DNS) сървър",
|
||||
"Resource Record Type": "Тип запис",
|
||||
"Last Result": "Последен резултат",
|
||||
"Create your admin account": "Създаване на администриращ акаунт",
|
||||
"Repeat Password": "Повторете паролата",
|
||||
"Import Backup": "Импорт на архив",
|
||||
"Export Backup": "Експорт на архив",
|
||||
Export: "Експорт",
|
||||
Import: "Импорт",
|
||||
respTime: "Време за отговор (ms)",
|
||||
notAvailableShort: "Няма",
|
||||
"Default enabled": "Включен по подразбиране",
|
||||
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
|
||||
Create: "Създай",
|
||||
"Clear Data": "Изтрий данни",
|
||||
Events: "Събития",
|
||||
Heartbeats: "Проверки",
|
||||
"Auto Get": "Автоматияно получаване",
|
||||
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
|
||||
backupDescription2: "PS: Данни за история и събития не са включени.",
|
||||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||
alertNoFile: "Моля, изберете файл за импортиране.",
|
||||
alertWrongFileType: "Моля, изберете JSON файл.",
|
||||
"Clear all statistics": "Изтрий цялата статистика",
|
||||
"Skip existing": "Пропусни съществуващите",
|
||||
Overwrite: "Презапиши",
|
||||
Options: "Опции",
|
||||
"Keep both": "Запази двете",
|
||||
"Verify Token": "Проверка на токен код",
|
||||
"Setup 2FA": "Настройка 2FA",
|
||||
"Enable 2FA": "Включи 2FA",
|
||||
"Disable 2FA": "Изключи 2FA",
|
||||
"2FA Settings": "Настройки 2FA",
|
||||
"Two Factor Authentication": "Двуфакторно удостоверяване",
|
||||
Active: "Активно",
|
||||
Inactive: "Неактивно",
|
||||
Token: "Токен код",
|
||||
"Show URI": "Покажи URI",
|
||||
Tags: "Етикети",
|
||||
"Add New below or Select...": "Добавете нов по-долу или изберете...",
|
||||
"Tag with this name already exist.": "Етикет с това име вече съществува.",
|
||||
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.",
|
||||
color: "цвят",
|
||||
"value (optional)": "стойност (по желание)",
|
||||
Gray: "Сиво",
|
||||
Red: "Червено",
|
||||
Orange: "Оранжево",
|
||||
Green: "Зелено",
|
||||
Blue: "Синьо",
|
||||
Indigo: "Индиго",
|
||||
Purple: "Лилаво",
|
||||
Pink: "Розово",
|
||||
"Search...": "Търси...",
|
||||
"Avg. Ping": "Ср. пинг",
|
||||
"Avg. Response": "Ср. отговор",
|
||||
"Entry Page": "Основна страница",
|
||||
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.",
|
||||
"No Services": "Няма Услуги",
|
||||
"All Systems Operational": "Всички услуги са достъпни",
|
||||
"Partially Degraded Service": "Част от услугите са недостъпни",
|
||||
"Degraded Service": "Всички услуги са недостъпни",
|
||||
"Add Group": "Добави група",
|
||||
"Add a monitor": "Добави монитор",
|
||||
"Edit Status Page": "Редактиране Статус страница",
|
||||
"Go to Dashboard": "Към Таблото",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Поддържа 50+ услуги за инвестяване)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Status Page": "Статус страница",
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
languageName: "Danish",
|
||||
languageName: "Danish (Danmark)",
|
||||
Settings: "Indstillinger",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Opdatering tilgængelig",
|
||||
@ -126,57 +126,76 @@ export default {
|
||||
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
|
||||
alertNoFile: "Vælg en fil der skal importeres.",
|
||||
alertWrongFileType: "Vælg venligst en JSON-fil.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
twoFAVerifyLabel: "Indtast venligst dit token for at bekræfte, at 2FA fungerer",
|
||||
tokenValidSettingsMsg: "Token er gyldigt! Du kan nu gemme 2FA -indstillingerne.",
|
||||
confirmEnableTwoFAMsg: "Er du sikker på at du vil aktivere 2FA?",
|
||||
confirmDisableTwoFAMsg: "Er du sikker på at du vil deaktivere 2FA?",
|
||||
"Apply on all existing monitors": "Anvend på alle eksisterende overvågere",
|
||||
"Verify Token": "Verificere Token",
|
||||
"Setup 2FA": "Opsæt 2FA",
|
||||
"Enable 2FA": "Aktiver 2FA",
|
||||
"Disable 2FA": "Deaktiver 2FA",
|
||||
"2FA Settings": "2FA Indstillinger",
|
||||
"Two Factor Authentication": "To-Faktor Autentificering",
|
||||
Active: "Aktive",
|
||||
Inactive: "Inaktive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
"Show URI": "Vis URI",
|
||||
"Clear all statistics": "Ryd alle Statistikker",
|
||||
retryCheckEverySecond: "Prøv igen hvert {0} sekund.",
|
||||
importHandleDescription: "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
|
||||
confirmImportMsg: "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Gentagelsesinterval",
|
||||
"Import Backup": "Importer Backup",
|
||||
"Export Backup": "Eksporter Backup",
|
||||
"Skip existing": "Spring over eksisterende",
|
||||
Overwrite: "Overskriv",
|
||||
Options: "Valgmuligheder",
|
||||
"Keep both": "Behold begge",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
"Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
|
||||
"Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
|
||||
"Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
|
||||
color: "farve",
|
||||
"value (optional)": "værdi (valgfri)",
|
||||
Gray: "Grå",
|
||||
Red: "Rød",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Green: "Grøn",
|
||||
Blue: "Blå",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Purple: "Lilla",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Search...": "Søg...",
|
||||
"Avg. Ping": "Gns. Ping",
|
||||
"Avg. Response": "Gns. Respons",
|
||||
"Entry Page": "Entry Side",
|
||||
statusPageNothing: "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
|
||||
"No Services": "Ingen Tjenester",
|
||||
"All Systems Operational": "Alle Systemer i Drift",
|
||||
"Partially Degraded Service": "Delvist Forringet Service",
|
||||
"Degraded Service": "Forringet Service",
|
||||
"Add Group": "Tilføj Gruppe",
|
||||
"Add a monitor": "Tilføj en Overvåger",
|
||||
"Edit Status Page": "Rediger Statusside",
|
||||
"Go to Dashboard": "Gå til Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Support 50+ Notification services)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
export default {
|
||||
languageName: "German",
|
||||
languageName: "Deutsch (Deutschland)",
|
||||
Settings: "Einstellungen",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Update Verfügbar",
|
||||
"New Update": "Update verfügbar",
|
||||
Language: "Sprache",
|
||||
Appearance: "Erscheinung",
|
||||
Theme: "Thema",
|
||||
General: "Allgemein",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Überprüfen von Updates auf Github",
|
||||
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
|
||||
List: "Liste",
|
||||
Add: "Hinzufügen",
|
||||
"Add New Monitor": "Neuer Monitor",
|
||||
@ -38,21 +38,21 @@ export default {
|
||||
checkEverySecond: "Überprüfe alle {0} Sekunden",
|
||||
Response: "Antwortzeit",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Monitor Typ",
|
||||
Keyword: "Schlüsselwort",
|
||||
"Monitor Type": "Monitor-Typ",
|
||||
Keyword: "Suchwort",
|
||||
"Friendly Name": "Anzeigename",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Taktintervall",
|
||||
"Heartbeat Interval": "Prüfintervall",
|
||||
Retries: "Wiederholungen",
|
||||
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
|
||||
Advanced: "Erweitert",
|
||||
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten",
|
||||
"Upside Down Mode": "Umgedrehter Modus",
|
||||
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.",
|
||||
ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
|
||||
"Upside Down Mode": "Invertierter Modus",
|
||||
upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
|
||||
"Max. Redirects": "Max. Weiterleitungen",
|
||||
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.",
|
||||
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
|
||||
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
|
||||
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
|
||||
Save: "Speichern",
|
||||
@ -62,27 +62,27 @@ export default {
|
||||
Light: "Hell",
|
||||
Dark: "Dunkel",
|
||||
Auto: "Auto",
|
||||
"Theme - Heartbeat Bar": "Thema - Taktleiste",
|
||||
"Theme - Heartbeat Bar": "Thema - Zeitleiste",
|
||||
Normal: "Normal",
|
||||
Bottom: "Unten",
|
||||
None: "Keine",
|
||||
Timezone: "Zeitzone",
|
||||
"Search Engine Visibility": "Suchmaschinensichtbarkeit",
|
||||
"Search Engine Visibility": "Sichtbarkeit für Suchmaschinen",
|
||||
"Allow indexing": "Indizierung zulassen",
|
||||
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
|
||||
"Change Password": "Passwort ändern",
|
||||
"Current Password": "Dezeitiges Passwort",
|
||||
"Current Password": "Derzeitiges Passwort",
|
||||
"New Password": "Neues Passwort",
|
||||
"Repeat New Password": "Wiederhole neues Passwort",
|
||||
"Repeat New Password": "Neues Passwort wiederholen",
|
||||
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
|
||||
"Update Password": "Ändere Passwort",
|
||||
"Update Password": "Passwort aktualisieren",
|
||||
"Disable Auth": "Authentifizierung deaktivieren",
|
||||
"Enable Auth": "Authentifizierung aktivieren",
|
||||
Logout: "Ausloggen",
|
||||
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
|
||||
Leave: "Verlassen",
|
||||
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
|
||||
Confirm: "Bestätige",
|
||||
Confirm: "Bestätigen",
|
||||
Yes: "Ja",
|
||||
No: "Nein",
|
||||
Username: "Benutzername",
|
||||
@ -91,30 +91,30 @@ export default {
|
||||
Login: "Einloggen",
|
||||
"No Monitors, please": "Keine Monitore, bitte",
|
||||
"add one": "hinzufügen",
|
||||
"Notification Type": "Benachrichtigungs Dienst",
|
||||
"Notification Type": "Benachrichtigungsdienst",
|
||||
Email: "E-Mail",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Zertifikatsinfo",
|
||||
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.",
|
||||
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?",
|
||||
keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.",
|
||||
deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
|
||||
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
|
||||
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
|
||||
"Resolver Server": "Auflösungsserver",
|
||||
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
|
||||
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?",
|
||||
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?",
|
||||
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 Statistiken für diesen Monitor löschen möchtest?",
|
||||
"Clear Data": "Lösche Daten",
|
||||
Events: "Ereignisse",
|
||||
Heartbeats: "Statistiken",
|
||||
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin Konto",
|
||||
confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
|
||||
"Create your admin account": "Erstelle dein Admin-Konto",
|
||||
"Repeat Password": "Wiederhole das Passwort",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
respTime: "Antw. Zeit (ms)",
|
||||
respTime: "Antw.-Zeit (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "Standardmäßig aktiviert",
|
||||
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
|
||||
@ -124,35 +124,35 @@ export default {
|
||||
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
|
||||
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
|
||||
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
||||
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
||||
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
||||
alertNoFile: "Bitte wähle eine Datei zum Importieren aus.",
|
||||
alertWrongFileType: "Bitte wähle eine JSON-Datei aus.",
|
||||
"Clear all statistics": "Lösche alle Statistiken",
|
||||
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
|
||||
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder jede Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
|
||||
"Skip existing": "Vorhandene überspringen",
|
||||
Overwrite: "Überschreiben",
|
||||
Options: "Optionen",
|
||||
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
|
||||
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import-Option ausgewählt ist.",
|
||||
"Keep both": "Beide behalten",
|
||||
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
|
||||
twoFAVerifyLabel: "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert",
|
||||
"Verify Token": "Token verifizieren",
|
||||
"Setup 2FA": "2FA Einrichten",
|
||||
"Enable 2FA": "2FA Aktivieren",
|
||||
"Setup 2FA": "2FA einrichten",
|
||||
"Enable 2FA": "2FA aktivieren",
|
||||
"Disable 2FA": "2FA deaktivieren",
|
||||
"2FA Settings": "2FA Einstellungen",
|
||||
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
|
||||
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
|
||||
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
|
||||
"Two Factor Authentication": "Zwei Faktor Authentifizierung",
|
||||
"2FA Settings": "2FA-Einstellungen",
|
||||
confirmEnableTwoFAMsg: "Bist du sicher, dass du 2FA aktivieren möchtest?",
|
||||
confirmDisableTwoFAMsg: "Bist du sicher, dass du 2FA deaktivieren möchtest?",
|
||||
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA-Einstellungen speichern.",
|
||||
"Two Factor Authentication": "Zwei-Faktor-Authentifizierung",
|
||||
Active: "Aktiv",
|
||||
Inactive: "Inaktiv",
|
||||
Token: "Token",
|
||||
"Show URI": "URI Anzeigen",
|
||||
"Show URI": "URI anzeigen",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...",
|
||||
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.",
|
||||
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.",
|
||||
"Add New below or Select...": "Bestehenden Tag auswählen oder neuen hinzufügen...",
|
||||
"Tag with this name already exist.": "Ein Tag mit diesem Namen existiert bereits.",
|
||||
"Tag with this value already exist.": "Ein Tag mit diesem Wert existiert bereits.",
|
||||
color: "Farbe",
|
||||
"value (optional)": "Wert (Optional)",
|
||||
"value (optional)": "Wert (optional)",
|
||||
Gray: "Grau",
|
||||
Red: "Rot",
|
||||
Orange: "Orange",
|
||||
@ -162,20 +162,39 @@ export default {
|
||||
Purple: "Lila",
|
||||
Pink: "Pink",
|
||||
"Search...": "Suchen...",
|
||||
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall",
|
||||
"Heartbeat Retry Interval": "Heartbeat-Wiederholungsintervall",
|
||||
retryCheckEverySecond: "Versuche alle {0} Sekunden",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Avg. Ping": "Durchsch. Ping",
|
||||
"Avg. Response": "Durchsch. Antwort",
|
||||
"Import Backup": "Backup importieren",
|
||||
"Export Backup": "Backup exportieren",
|
||||
"Avg. Ping": "Durchschn. Ping",
|
||||
"Avg. Response": "Durchschn. Antwort",
|
||||
"Entry Page": "Einstiegsseite",
|
||||
"statusPageNothing": "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
|
||||
statusPageNothing: "Noch ist hier nichts. Bitte füge eine Gruppe oder einen Monitor hinzu.",
|
||||
"No Services": "Keine Dienste",
|
||||
"All Systems Operational": "Alle Systeme Betriebsbereit",
|
||||
"All Systems Operational": "Alle Systeme betriebsbereit",
|
||||
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
|
||||
"Degraded Service": "Eingeschränkter Dienst",
|
||||
"Add Group": "Gruppe hinzufügen",
|
||||
"Add a monitor": "Monitor hinzufügen",
|
||||
"Edit Status Page": "Bearbeite Statusseite",
|
||||
"Edit Status Page": "Bearbeite Status-Seite",
|
||||
"Go to Dashboard": "Gehe zum Dashboard",
|
||||
"Status Page": "Status-Seite",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "E-Mail (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Unterstützung für 50+ Benachrichtigungsdienste)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ export default {
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
confirmClearStatisticsMsg: "Are you sure you want to delete ALL statistics?",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
@ -33,6 +33,7 @@ export default {
|
||||
Appearance: "Appearance",
|
||||
Theme: "Theme",
|
||||
General: "General",
|
||||
"Primary Base URL": "Primary Base URL",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Check Update On GitHub",
|
||||
List: "List",
|
||||
@ -75,6 +76,9 @@ export default {
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Max. Redirects": "Max. Redirects",
|
||||
"Accepted Status Codes": "Accepted Status Codes",
|
||||
"Push URL": "Push URL",
|
||||
needPushEvery: "You should call this url every {0} seconds.",
|
||||
pushOptionalParams: "Optional parameters: {0}",
|
||||
Save: "Save",
|
||||
Notifications: "Notifications",
|
||||
"Not available, please setup.": "Not available, please setup.",
|
||||
@ -169,7 +173,7 @@ export default {
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
@ -178,5 +182,127 @@ export default {
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"Status Page": "Status Page",
|
||||
// Start notification form
|
||||
defaultNotificationName: "My {notification} Alert ({number})",
|
||||
here: "here",
|
||||
"Required": "Required",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
wayToGetTelegramToken: "You can get a token from {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
|
||||
wayToGetTelegramChatID: "You can get your chat id by sending message to the bot and go to this url to view the chat_id:",
|
||||
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
|
||||
chatIDNotFound: "Chat ID is not found, please send a message to this bot first",
|
||||
"webhook": "Webhook",
|
||||
"Post URL": "Post URL",
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} is good for any modern http servers such as express.js",
|
||||
webhookFormDataDesc: "{multipart} is good for PHP, you just need to parse the json by {decodeFunction}",
|
||||
"smtp": "Email (SMTP)",
|
||||
secureOptionNone: "None / STARTTLS (25, 587)",
|
||||
secureOptionTLS: "TLS (465)",
|
||||
"Ignore TLS Error": "Ignore TLS Error",
|
||||
"From Email": "From Email",
|
||||
emailCustomSubject: "Custom Subject",
|
||||
"To Email": "To Email",
|
||||
smtpCC: "CC",
|
||||
smtpBCC: "BCC",
|
||||
"discord": "Discord",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
wayToGetDiscordURL: "You can get this by going to Server Settings -> Integrations -> Create Webhook",
|
||||
"Bot Display Name": "Bot Display Name",
|
||||
"Prefix Custom Message": "Prefix Custom Message",
|
||||
"Hello @everyone is...": "Hello {'@'}everyone is...",
|
||||
"teams": "Microsoft Teams",
|
||||
"Webhook URL": "Webhook URL",
|
||||
wayToGetTeamsURL: "You can learn how to create a webhook url {0}.",
|
||||
"signal": "Signal",
|
||||
"Number": "Number",
|
||||
"Recipients": "Recipients",
|
||||
needSignalAPI: "You need to have a signal client with REST API.",
|
||||
wayToCheckSignalURL: "You can check this url to view how to setup one:",
|
||||
signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||
"gotify": "Gotify",
|
||||
"Application Token": "Application Token",
|
||||
"Server URL": "Server URL",
|
||||
"Priority": "Priority",
|
||||
"slack": "Slack",
|
||||
"Icon Emoji": "Icon Emoji",
|
||||
"Channel Name": "Channel Name",
|
||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||
aboutWebhooks: "More info about webhooks on: {0}",
|
||||
aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the webhook channel. Ex: #other-channel",
|
||||
aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.",
|
||||
emojiCheatSheet: "Emoji cheat sheet: {0}",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Support 50+ Notification services)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"User Key": "User Key",
|
||||
"Device": "Device",
|
||||
"Message Title": "Message Title",
|
||||
"Notification Sound": "Notification Sound",
|
||||
"More info on:": "More info on: {0}",
|
||||
pushoverDesc1: "Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.",
|
||||
pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.",
|
||||
"SMS Type": "SMS Type",
|
||||
octopushTypePremium: "Premium (Fast - recommended for alerting)",
|
||||
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)",
|
||||
checkPrice: "Check {0} prices:",
|
||||
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
|
||||
"Check octopush prices": "Check octopush prices {0}.",
|
||||
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
|
||||
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "LunaSea Device ID",
|
||||
"Apprise URL": "Apprise URL",
|
||||
"Example:": "Example: {0}",
|
||||
"Read more:": "Read more: {0}",
|
||||
"Status:": "Status: {0}",
|
||||
"Read more": "Read more",
|
||||
appriseInstalled: "Apprise is installed.",
|
||||
appriseNotInstalled: "Apprise is not installed. {0}",
|
||||
"Access Token": "Access Token",
|
||||
"Channel access token": "Channel access token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
lineDevConsoleTo: "Line Developers Console - {0}",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"User ID": "User ID",
|
||||
"Messaging API": "Messaging API",
|
||||
wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.",
|
||||
"Icon URL": "Icon URL",
|
||||
aboutIconURL: "You can provide a link to a picture in \"Icon URL\" to override the default profile picture. Will not be used if Icon Emoji is set.",
|
||||
aboutMattermostChannelName: "You can override the default channel that webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel",
|
||||
"matrix": "Matrix",
|
||||
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
|
||||
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
|
||||
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use Your Sender Name (You need to register name first). Reliable for alerts.",
|
||||
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
|
||||
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
|
||||
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
"Feishu WebHookUrl": "Feishu WebHookUrl",
|
||||
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
|
||||
"Internal Room Id": "Internal Room Id",
|
||||
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
|
||||
// End notification form
|
||||
Method: "Method",
|
||||
Body: "Body",
|
||||
Headers: "Headers",
|
||||
PushUrl: "Push URL",
|
||||
HeadersInvalidFormat: "The request headers are not valid JSON: ",
|
||||
BodyInvalidFormat: "The request body is not valid JSON: ",
|
||||
"Monitor History": "Monitor History:",
|
||||
clearDataOlderThan: "Keep monitor history data for {0} days.",
|
||||
records: "records",
|
||||
"One record": "One record",
|
||||
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records",
|
||||
steamApiKeyDescription: "For monitoring a Steam Gameserver you need a steam Web-API key. You can register your api key here: https://steamcommunity.com/dev",
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
passwordNotMatchMsg: "La contraseña repetida no coincide.",
|
||||
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
|
||||
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
|
||||
pauseDashboardHome: "Pausar",
|
||||
pauseDashboardHome: "Pausado",
|
||||
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
|
||||
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
|
||||
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
|
||||
@ -17,7 +17,7 @@ export default {
|
||||
pauseMonitorMsg: "¿Seguro que quieres pausar?",
|
||||
Settings: "Ajustes",
|
||||
Dashboard: "Panel",
|
||||
"New Update": "Vueva actualización",
|
||||
"New Update": "Nueva actualización",
|
||||
Language: "Idioma",
|
||||
Appearance: "Apariencia",
|
||||
Theme: "Tema",
|
||||
@ -32,7 +32,7 @@ export default {
|
||||
Down: "Caído",
|
||||
Pending: "Pendiente",
|
||||
Unknown: "Desconocido",
|
||||
Pause: "Pausa",
|
||||
Pause: "Pausar",
|
||||
Name: "Nombre",
|
||||
Status: "Estado",
|
||||
DateTime: "Fecha y Hora",
|
||||
@ -61,11 +61,11 @@ export default {
|
||||
Retries: "Reintentos",
|
||||
Advanced: "Avanzado",
|
||||
"Upside Down Mode": "Modo invertido",
|
||||
"Max. Redirects": "Máx. redirecciones",
|
||||
"Max. Redirects": "Redirecciones Máximas",
|
||||
"Accepted Status Codes": "Códigos de estado aceptados",
|
||||
Save: "Guardar",
|
||||
Notifications: "Notificaciones",
|
||||
"Not available, please setup.": "No disponible, por favor configurar.",
|
||||
"Not available, please setup.": "No disponible, por favor configúrelo.",
|
||||
"Setup Notification": "Configurar notificación",
|
||||
Light: "Claro",
|
||||
Dark: "Oscuro",
|
||||
@ -87,7 +87,7 @@ export default {
|
||||
"Enable Auth": "Habilitar Autenticación",
|
||||
Logout: "Cerrar sesión",
|
||||
Leave: "Salir",
|
||||
"I understand, please disable": "Lo comprendo, por favor deshabilitar",
|
||||
"I understand, please disable": "Entiendo, por favor deshabilitar",
|
||||
Confirm: "Confirmar",
|
||||
Yes: "Sí",
|
||||
No: "No",
|
||||
@ -108,75 +108,99 @@ export default {
|
||||
"Repeat Password": "Repetir contraseña",
|
||||
respTime: "Tiempo de resp. (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
Create: "Create",
|
||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
||||
"Clear Data": "Clear Data",
|
||||
Events: "Events",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
"Default enabled": "Default enabled",
|
||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||
backupDescription2: "PS: History and event data is not included.",
|
||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||
alertNoFile: "Please select a file to import.",
|
||||
alertWrongFileType: "Please select a JSON file.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Create: "Crear",
|
||||
clearEventsMsg: "¿Está seguro de que desea eliminar todos los eventos de este monitor?",
|
||||
clearHeartbeatsMsg: "¿Está seguro de que desea eliminar todos los latidos de este monitor?",
|
||||
confirmClearStatisticsMsg: "¿Está seguro de que desea eliminar TODAS las estadísticas?",
|
||||
"Clear Data": "Borrar Datos",
|
||||
Events: "Eventos",
|
||||
Heartbeats: "Latidos",
|
||||
"Auto Get": "Obtener automáticamente",
|
||||
enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puede deshabilitar la notificación por separado para cada monitor.",
|
||||
"Default enabled": "Habilitado por defecto",
|
||||
"Also apply to existing monitors": "También se aplica a monitores existentes",
|
||||
Export: "Exportar",
|
||||
Import: "Importar",
|
||||
backupDescription: "Puede hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.",
|
||||
backupDescription2: "PD: el historial y los datos de eventos no están incluidos.",
|
||||
backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdelo con cuidado.",
|
||||
alertNoFile: "Seleccione un archivo para importar.",
|
||||
alertWrongFileType: "Seleccione un archivo JSON.",
|
||||
twoFAVerifyLabel: "Ingrese su token para verificar que 2FA está funcionando",
|
||||
tokenValidSettingsMsg: "¡El token es válido! Ahora puede guardar la configuración de 2FA.",
|
||||
confirmEnableTwoFAMsg: "¿Estás seguro de que quieres habilitar 2FA?",
|
||||
confirmDisableTwoFAMsg: "¿Estás seguro de que quieres desactivar 2FA?",
|
||||
"Apply on all existing monitors": "Aplicar en todos los monitores existentes",
|
||||
"Verify Token": "Verificar token",
|
||||
"Setup 2FA": "Configurar 2FA",
|
||||
"Enable 2FA": "Habilitar 2FA",
|
||||
"Disable 2FA": "Desactivar 2FA",
|
||||
"2FA Settings": "Ajustes 2FA",
|
||||
"Two Factor Authentication": "Autenticación de dos factores",
|
||||
Active: "Activo",
|
||||
Inactive: "Inactivo",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
"Show URI": "Mostrar URI",
|
||||
"Clear all statistics": "Borrar todas las estadísticas",
|
||||
retryCheckEverySecond: "Reintentar cada {0} segundo.",
|
||||
importHandleDescription: "Elija 'Omitir existente' si desea omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.",
|
||||
confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrese de haber seleccionado la opción de importación correcta.",
|
||||
"Heartbeat Retry Interval": "Intervalo de reintento de latido",
|
||||
"Import Backup": "Importar copia de seguridad",
|
||||
"Export Backup": "Exportar copia de seguridad",
|
||||
"Skip existing": "Omitir existente",
|
||||
Overwrite: "Sobrescribir",
|
||||
Options: "Opciones",
|
||||
"Keep both": "Mantén ambos",
|
||||
Tags: "Etiquetas",
|
||||
"Add New below or Select...": "Agregar nuevo a continuación o Seleccionar...",
|
||||
"Tag with this name already exist.": "La etiqueta con este nombre ya existe.",
|
||||
"Tag with this value already exist.": "La etiqueta con este valor ya existe.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
"value (optional)": "valor (opcional)",
|
||||
Gray: "Gris",
|
||||
Red: "Rojo",
|
||||
Orange: "Naranja",
|
||||
Green: "Verde",
|
||||
Blue: "Azul",
|
||||
Indigo: "Índigo",
|
||||
Purple: "Morado",
|
||||
Pink: "Rosa",
|
||||
"Search...": "Buscar...",
|
||||
"Avg. Ping": "Ping promedio",
|
||||
"Avg. Response": "Respuesta promedio",
|
||||
"Entry Page": "Página de entrada",
|
||||
statusPageNothing: "No hay nada aquí, agregue un grupo o un monitor.",
|
||||
"No Services": "Sin servicio",
|
||||
"All Systems Operational": "Todos los sistemas están operativos",
|
||||
"Partially Degraded Service": "Servicio parcialmente degradado",
|
||||
"Degraded Service": "Servicio degradado",
|
||||
"Add Group": "Agregar Grupo",
|
||||
"Add a monitor": "Agregar un monitor",
|
||||
"Edit Status Page": "Editar página de estado",
|
||||
"Go to Dashboard": "Ir al panel de control",
|
||||
"Status Page": "Página de estado",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Admite más de 50 servicios de notificación)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Monitor History": "Historial de monitor:",
|
||||
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
|
||||
records: "registros",
|
||||
"One record": "Un registro",
|
||||
"Showing {from} to {to} of {count} records": "Mostrando desde {from} a {to} de {count} registros",
|
||||
};
|
||||
|
@ -1,21 +1,22 @@
|
||||
export default {
|
||||
languageName: "eesti",
|
||||
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
|
||||
retryCheckEverySecond: "Kontrolli {0} sekundilise vahega.",
|
||||
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
|
||||
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
|
||||
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
|
||||
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
|
||||
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
|
||||
passwordNotMatchMsg: "Salasõnad ei kattu.",
|
||||
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
|
||||
notificationDescription: "Teavitusteenuse kasutamiseks seo see seirega.",
|
||||
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
|
||||
pauseDashboardHome: "Seismas",
|
||||
pauseDashboardHome: "Seisatud",
|
||||
deleteMonitorMsg: "Kas soovid eemaldada seire?",
|
||||
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
|
||||
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusteenuse kõikidelt seiretelt?",
|
||||
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
|
||||
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
|
||||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||
Settings: "Seaded",
|
||||
"Status Page": "Ülevaade",
|
||||
Dashboard: "Töölaud",
|
||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||
Language: "Keel",
|
||||
@ -26,20 +27,21 @@ export default {
|
||||
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
|
||||
List: "Nimekiri",
|
||||
Add: "Lisa",
|
||||
"Add New Monitor": "Seire lisamine",
|
||||
"Add New Monitor": "Lisa seire",
|
||||
"Add a monitor": "Lisa seire",
|
||||
"Quick Stats": "Ülevaade",
|
||||
Up: "Töökorras",
|
||||
Down: "Rikkis",
|
||||
Pending: "Määramisel",
|
||||
Unknown: "Teadmata",
|
||||
Pause: "Seiskamine",
|
||||
Unknown: "Kahtlast",
|
||||
Pause: "Seiska",
|
||||
Name: "Nimi",
|
||||
Status: "Olek",
|
||||
DateTime: "Kuupäev",
|
||||
Message: "Tulemus",
|
||||
"No important events": "Märkimisväärsed juhtumid puuduvad.",
|
||||
Resume: "Taasta",
|
||||
Edit: "Muutmine",
|
||||
Edit: "Muuda",
|
||||
Delete: "Eemalda",
|
||||
Current: "Hetkeseisund",
|
||||
Uptime: "Eluiga",
|
||||
@ -49,7 +51,7 @@ export default {
|
||||
"-day": "-päev",
|
||||
hour: "tund",
|
||||
"-hour": "-tund",
|
||||
Response: "Vastus",
|
||||
Response: "Reaktsiooniaeg",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Seire tüüp",
|
||||
Keyword: "Võtmesõna",
|
||||
@ -108,14 +110,14 @@ export default {
|
||||
"Repeat Password": "korda salasõna",
|
||||
respTime: "Reageerimisaeg (ms)",
|
||||
notAvailableShort: "N/A",
|
||||
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
|
||||
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusteenuset. Seiretelt võib teavitusteenuse ühekaupa eemaldada.",
|
||||
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
|
||||
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
|
||||
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
|
||||
confirmClearStatisticsMsg: "Kas soovid TERVE ajaloo kustutada?",
|
||||
Export: "Eksport",
|
||||
Import: "Import",
|
||||
"Default enabled": "Kasuta vaikimisi",
|
||||
"Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
|
||||
"Apply on all existing monitors": "Kõik praegused seired hakkavad kasutama seda teavitusteenust",
|
||||
Create: "Loo konto",
|
||||
"Clear Data": "Eemalda andmed",
|
||||
Events: "Sündmused",
|
||||
@ -123,60 +125,76 @@ export default {
|
||||
"Auto Get": "Hangi automaatselt",
|
||||
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
|
||||
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
|
||||
backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
|
||||
backupDescription3: "Varukoopiad sisaldavad teavitusteenusete pääsuvõtmeid.",
|
||||
alertNoFile: "Palun lisa fail, mida importida.",
|
||||
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
Tags: "Tags",
|
||||
"Add New below or Select...": "Add New below or Select...",
|
||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||
color: "color",
|
||||
"value (optional)": "value (optional)",
|
||||
Gray: "Gray",
|
||||
Red: "Red",
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Blue: "Blue",
|
||||
Indigo: "Indigo",
|
||||
Purple: "Purple",
|
||||
Pink: "Pink",
|
||||
"Search...": "Search...",
|
||||
"Avg. Ping": "Avg. Ping",
|
||||
"Avg. Response": "Avg. Response",
|
||||
"Entry Page": "Entry Page",
|
||||
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||
"No Services": "No Services",
|
||||
"All Systems Operational": "All Systems Operational",
|
||||
"Partially Degraded Service": "Partially Degraded Service",
|
||||
"Degraded Service": "Degraded Service",
|
||||
"Add Group": "Add Group",
|
||||
"Add a monitor": "Add a monitor",
|
||||
"Edit Status Page": "Edit Status Page",
|
||||
"Go to Dashboard": "Go to Dashboard",
|
||||
twoFAVerifyLabel: "2FA kinnitamiseks sisesta pääsukood",
|
||||
tokenValidSettingsMsg: "Kood õige. Akna võib sulgeda.",
|
||||
confirmEnableTwoFAMsg: "Kas soovid 2FA sisse lülitada?",
|
||||
confirmDisableTwoFAMsg: "Kas soovid 2FA välja lülitada?",
|
||||
"Verify Token": "Kontrolli",
|
||||
"Setup 2FA": "Kaksikautentimise seadistamine",
|
||||
"Enable 2FA": "Seadista 2FA",
|
||||
"Disable 2FA": "Lülita 2FA välja",
|
||||
"2FA Settings": "2FA seaded",
|
||||
"Two Factor Authentication": "Kaksikautentimine",
|
||||
Active: "kasutusel",
|
||||
Inactive: "seadistamata",
|
||||
Token: "kaksikautentimise kood",
|
||||
"Show URI": "Näita URId",
|
||||
"Clear all statistics": "Tühjenda ajalugu",
|
||||
importHandleDescription: "'kombineeri' täiendab varukoopiast ja kirjutab üle samanimelised seireid ja teavitusteenused; 'lisa praegustele' jätab olemasolevad puutumata; 'asenda' kustutab ja asendab kõik seired ja teavitusteenused.",
|
||||
confirmImportMsg: "Käkerdistest hoidumiseks lae enne taastamist alla uus varukoopia. Kas soovid taastada üles laetud?",
|
||||
"Heartbeat Retry Interval": "Korduskatsete intervall",
|
||||
"Import Backup": "Varukoopia importimine",
|
||||
"Export Backup": "Varukoopia eksportimine",
|
||||
"Skip existing": "lisa praegustele",
|
||||
Overwrite: "asenda",
|
||||
Options: "Mestimisviis",
|
||||
"Keep both": "kombineeri",
|
||||
Tags: "Sildid",
|
||||
"Add New below or Select...": "Leia või lisa all uus…",
|
||||
"Tag with this name already exist.": "Selle nimega silt on juba olemas.",
|
||||
"Tag with this value already exist.": "Selle väärtusega silt on juba olemas.",
|
||||
color: "värvus",
|
||||
"value (optional)": "väärtus (fakultatiivne)",
|
||||
Gray: "hall",
|
||||
Red: "punane",
|
||||
Orange: "oranž",
|
||||
Green: "roheline",
|
||||
Blue: "sinine",
|
||||
Indigo: "indigo",
|
||||
Purple: "lilla",
|
||||
Pink: "roosa",
|
||||
"Search...": "Otsi…",
|
||||
"Avg. Ping": "Keskmine ping",
|
||||
"Avg. Response": "Keskmine reaktsiooniaeg",
|
||||
"Entry Page": "Avaleht",
|
||||
statusPageNothing: "Kippu ega kõppu; siia saab lisada seireid või -gruppe.",
|
||||
"No Services": "Teenused puuduvad.",
|
||||
"All Systems Operational": "Kõik töökorras",
|
||||
"Partially Degraded Service": "Teenuse töö osaliselt häiritud",
|
||||
"Degraded Service": "Teenuse töö häiritud",
|
||||
"Add Group": "Lisa grupp",
|
||||
"Edit Status Page": "Muuda lehte",
|
||||
"Go to Dashboard": "Töölauale",
|
||||
checkEverySecond: "Kontrolli peale tõrget {0} sekundilise vahega.",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "elektronpost (SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
gotify: "Gotify",
|
||||
slack: "Slack",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (vahendab üle 65 teavitusteenust)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "LINE",
|
||||
mattermost: "Mattermost",
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user