mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-28 01:04:05 +00:00
Merge branch 'louislam-master'
This commit is contained in:
commit
359fe52c2e
133 changed files with 15748 additions and 5164 deletions
|
@ -2,8 +2,12 @@
|
||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/out
|
||||||
|
/test
|
||||||
|
/kubernetes
|
||||||
/.do
|
/.do
|
||||||
**/.dockerignore
|
**/.dockerignore
|
||||||
|
/private
|
||||||
**/.git
|
**/.git
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
**/docker-compose*
|
**/docker-compose*
|
||||||
|
@ -15,7 +19,6 @@ README.md
|
||||||
.eslint*
|
.eslint*
|
||||||
.stylelint*
|
.stylelint*
|
||||||
/.github
|
/.github
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
app.json
|
app.json
|
||||||
CODE_OF_CONDUCT.md
|
CODE_OF_CONDUCT.md
|
||||||
|
|
26
.eslintrc.js
26
.eslintrc.js
|
@ -1,4 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
commonjs: true,
|
commonjs: true,
|
||||||
|
@ -16,6 +17,7 @@ module.exports = {
|
||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
"camelcase": ["warn", {
|
"camelcase": ["warn", {
|
||||||
"properties": "never",
|
"properties": "never",
|
||||||
"ignoreImports": true
|
"ignoreImports": true
|
||||||
|
@ -32,11 +34,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quotes: ["warn", "double"],
|
quotes: ["warn", "double"],
|
||||||
//semi: ['off', 'never'],
|
semi: "warn",
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
"vue/html-indent": ["warn", 4], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
|
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||||
"no-multi-spaces": ["error", {
|
"no-multi-spaces": ["error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
|
@ -84,10 +87,27 @@ module.exports = {
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [ "src/languages/*.js" ],
|
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||||
"rules": {
|
"rules": {
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
|
|
1
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
1
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
|
@ -16,4 +16,3 @@ Docker Version:
|
||||||
Node.js Version (Without Docker only):
|
Node.js Version (Without Docker only):
|
||||||
OS:
|
OS:
|
||||||
Browser:
|
Browser:
|
||||||
|
|
||||||
|
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
|
@ -23,7 +24,6 @@ Steps to reproduce the behavior:
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
|
||||||
**Info**
|
**Info**
|
||||||
Uptime Kuma Version:
|
Uptime Kuma Version:
|
||||||
Using Docker?: Yes/No
|
Using Docker?: Yes/No
|
||||||
|
@ -32,13 +32,11 @@ Node.js Version (Without Docker only):
|
||||||
OS:
|
OS:
|
||||||
Browser:
|
Browser:
|
||||||
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Error Log**
|
**Error Log**
|
||||||
It is easier for us to find out the problem.
|
It is easier for us to find out the problem.
|
||||||
|
|
||||||
Docker: "docker logs <container id>"
|
Docker: `docker logs <container id>`
|
||||||
PM2: "~/.pm2/logs/" (e.g. /home/ubuntu/.pm2/logs)
|
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
||||||
|
|
||||||
|
|
34
.github/workflows/auto-test.yml
vendored
Normal file
34
.github/workflows/auto-test.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x, 15.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 ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm test
|
||||||
|
env:
|
||||||
|
HEADLESS_TEST: 1
|
||||||
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -7,4 +7,8 @@ dist-ssr
|
||||||
|
|
||||||
/data
|
/data
|
||||||
!/data/.gitkeep
|
!/data/.gitkeep
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
/private
|
||||||
|
/out
|
||||||
|
/tmp
|
||||||
|
|
|
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|
132
CONTRIBUTING.md
132
CONTRIBUTING.md
|
@ -2,13 +2,32 @@
|
||||||
|
|
||||||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||||
|
|
||||||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||||
|
|
||||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
|
||||||
|
|
||||||
|
# Key Technical Skills
|
||||||
|
|
||||||
|
- Node.js (You should know 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?
|
# 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 to the master branch once it is tested.
|
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.
|
If you are not sure, feel free to create an empty pull request draft first.
|
||||||
|
|
||||||
|
@ -20,13 +39,13 @@ If you are not sure, feel free to create an empty pull request draft first.
|
||||||
- Add a chart
|
- Add a chart
|
||||||
- Fix a bug
|
- Fix a bug
|
||||||
|
|
||||||
### *️⃣ Requires one more reviewer
|
### *️⃣ Requires one more reviewer
|
||||||
|
|
||||||
I do not have such knowledge to test it.
|
I do not have such knowledge to test it.
|
||||||
|
|
||||||
- Add k8s supports
|
- Add k8s supports
|
||||||
|
|
||||||
### *️⃣ Low Priority
|
### *️⃣ Low Priority
|
||||||
|
|
||||||
It changed my current workflow and require further studies.
|
It changed my current workflow and require further studies.
|
||||||
|
|
||||||
|
@ -41,19 +60,18 @@ It changed my current workflow and require further studies.
|
||||||
|
|
||||||
# Project Styles
|
# Project Styles
|
||||||
|
|
||||||
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||||
|
|
||||||
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
|
- 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
|
- 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.
|
- Settings should be configurable in the frontend. Env var is not encouraged.
|
||||||
- Easy to use
|
- Easy to use
|
||||||
|
|
||||||
# Coding Styles
|
# Coding Styles
|
||||||
|
|
||||||
- Follow .editorconfig
|
- 4 spaces indentation
|
||||||
- Follow eslint
|
- Follow `.editorconfig`
|
||||||
|
- Follow ESLint
|
||||||
|
|
||||||
## Name convention
|
## Name convention
|
||||||
|
|
||||||
|
@ -62,64 +80,51 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
||||||
- CSS/SCSS: dash-type
|
- CSS/SCSS: dash-type
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
|
|
||||||
- Node.js >= 14
|
- Node.js >= 14
|
||||||
- Git
|
- Git
|
||||||
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
|
- IDE that supports ESLint and EditorConfig (I am using Intellji Idea)
|
||||||
- A SQLite tool (I am using SQLite Expert Personal)
|
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --dev
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
For npm@7, you need --legacy-peer-deps
|
# How to start the Backend Dev Server
|
||||||
|
|
||||||
```
|
(2021-09-23 Update)
|
||||||
npm install --legacy-peer-deps --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
# Backend Dev
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-server
|
npm run start-server-dev
|
||||||
|
|
||||||
# Or
|
|
||||||
|
|
||||||
node server/server.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It binds to 0.0.0.0:3001 by default.
|
It binds to `0.0.0.0:3001` by default.
|
||||||
|
|
||||||
|
|
||||||
## Backend Details
|
## Backend Details
|
||||||
|
|
||||||
It is mainly a socket.io app + express.js.
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||||
|
|
||||||
# Frontend Dev
|
- 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.
|
# How to start the Frontend Dev Server
|
||||||
|
|
||||||
```bash
|
1. Set the env var `NODE_ENV` to "development".
|
||||||
npm run dev
|
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 Devtool 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".
|
|
||||||
|
|
||||||
|
You can use Vue.js devtools Chrome extension for debugging.
|
||||||
|
|
||||||
## Build the frontend
|
## Build the frontend
|
||||||
|
|
||||||
|
@ -131,22 +136,39 @@ npm run build
|
||||||
|
|
||||||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||||
|
|
||||||
The router in "src/main.js"
|
The router is in `src/router.js`
|
||||||
|
|
||||||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
|
||||||
|
|
||||||
The data and socket logic in "src/mixins/socket.js"
|
The data and socket logic are in `src/mixins/socket.js`.
|
||||||
|
|
||||||
|
|
||||||
# Database Migration
|
# Database Migration
|
||||||
|
|
||||||
1. create `patch{num}.sql` in `./db/`
|
1. Create `patch-{name}.sql` in `./db/`
|
||||||
1. update `latestVersion` in `./server/database.js`
|
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.
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
23
README.md
23
README.md
|
@ -1,6 +1,7 @@
|
||||||
# Uptime Kuma
|
# 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%">
|
<div align="center" width="100%">
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
|
@ -20,12 +21,11 @@ It is a 5 minutes live demo, all data will be deleted after that. The server is
|
||||||
|
|
||||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||||
|
|
||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
|
||||||
* 20 seconds interval.
|
* 20 seconds interval.
|
||||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||||
|
|
||||||
|
@ -45,6 +45,9 @@ Browse to http://localhost:3001 after started.
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools: Node.js >= 14, git and pm2.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Update your npm to the latest version
|
||||||
|
npm install npm -g
|
||||||
|
|
||||||
git clone https://github.com/louislam/uptime-kuma.git
|
git clone https://github.com/louislam/uptime-kuma.git
|
||||||
cd uptime-kuma
|
cd uptime-kuma
|
||||||
npm run setup
|
npm run setup
|
||||||
|
@ -65,7 +68,6 @@ If you need more options or need to browse via a reserve proxy, please read:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||||
|
|
||||||
|
|
||||||
## 🆙 How to Update
|
## 🆙 How to Update
|
||||||
|
|
||||||
Please read:
|
Please read:
|
||||||
|
@ -107,15 +109,15 @@ Telegram Notification Sample:
|
||||||
|
|
||||||
If you love this project, please consider giving me a ⭐.
|
If you love this project, please consider giving me a ⭐.
|
||||||
|
|
||||||
|
|
||||||
## 🗣️ Discussion
|
## 🗣️ Discussion
|
||||||
|
|
||||||
You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
### Issues Page
|
||||||
|
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
Alternatively, you can discuss in my original post on reddit: https://www.reddit.com/r/selfhosted/comments/oi7dc7/uptime_kuma_a_fancy_selfhosted_monitoring_tool_an/
|
|
||||||
|
|
||||||
I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments.
|
|
||||||
|
|
||||||
|
### Subreddit
|
||||||
|
My Reddit account: louislamlam
|
||||||
|
You can mention me if you ask question on Reddit.
|
||||||
|
https://www.reddit.com/r/UptimeKuma/
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
@ -126,4 +128,3 @@ If you want to translate Uptime Kuma into your langauge, please read: https://gi
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,6 @@ currently being supported with security updates.
|
||||||
| 1.x.x | :white_check_mark: |
|
| 1.x.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
Please report security issues to uptime@kuma.pet.
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/issues
|
Do not use the issue tracker or discuss it in the public as it will cause more damage.
|
||||||
|
|
BIN
db/demo_kuma.db
BIN
db/demo_kuma.db
Binary file not shown.
7
db/patch-add-retry-interval-monitor.sql
Normal file
7
db/patch-add-retry-interval-monitor.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD retry_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
30
db/patch-group-table.sql
Normal file
30
db/patch-group-table.sql
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table `group`
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
constraint group_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
name VARCHAR(255) not null,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
public BOOLEAN default 0 not null,
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [monitor_group]
|
||||||
|
(
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [fk]
|
||||||
|
ON [monitor_group] (
|
||||||
|
[monitor_id],
|
||||||
|
[group_id]);
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-incident-table.sql
Normal file
18
db/patch-incident-table.sql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table incident
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
constraint incident_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
title VARCHAR(255) not null,
|
||||||
|
content TEXT not null,
|
||||||
|
style VARCHAR(30) default 'warning' not null,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
last_updated_date DATETIME,
|
||||||
|
pin BOOLEAN default 1 not null,
|
||||||
|
active BOOLEAN default 1 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
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;
|
19
db/patch10.sql
Normal file
19
db/patch10.sql
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
CREATE TABLE tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
color VARCHAR(255) NOT NULL,
|
||||||
|
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE monitor_tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
value TEXT,
|
||||||
|
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
|
||||||
|
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);
|
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/*
|
56
dockerfile
56
dockerfile
|
@ -1,37 +1,49 @@
|
||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
FROM louislam/uptime-kuma:base-debian AS build
|
||||||
FROM node:14-buster-slim AS build
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
|
||||||
# do not modify it, since we don't want to re-compile the arm prebuilt again
|
|
||||||
RUN apt update && \
|
|
||||||
apt --yes install python3 python3-pip python3-dev git g++ make && \
|
|
||||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
|
||||||
npm install mapbox/node-sqlite3#593c9d --build-from-source
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install --legacy-peer-deps && npm run build && npm prune --production
|
RUN npm install --legacy-peer-deps && \
|
||||||
|
npm run build && \
|
||||||
|
npm prune --production && \
|
||||||
|
chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
FROM node:14-bullseye-slim AS release
|
|
||||||
|
FROM louislam/uptime-kuma:base-debian AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Apprise,
|
|
||||||
# add sqlite3 cli for debugging in the future
|
|
||||||
# iputils-ping for ping
|
|
||||||
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 && \
|
|
||||||
pip3 --no-cache-dir install apprise && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy app files from build layer
|
# Copy app files from build layer
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-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=$DIST
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
FROM louislam/uptime-kuma:base-alpine AS build
|
||||||
FROM node:14-alpine3.12 AS build
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
|
||||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
|
|
||||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
|
||||||
npm install mapbox/node-sqlite3#593c9d && \
|
|
||||||
apk del .build-deps && \
|
|
||||||
rm -f /usr/bin/python
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install --legacy-peer-deps && npm run build && npm prune --production
|
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
|
FROM louislam/uptime-kuma:base-alpine AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install apprise
|
|
||||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
|
||||||
pip3 --no-cache-dir install apprise && \
|
|
||||||
rm -rf /root/.cache
|
|
||||||
|
|
||||||
# Copy app files from build layer
|
# Copy app files from build layer
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
|
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
|
|
21
extra/entrypoint.sh
Normal file
21
extra/entrypoint.sh
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# set -e Exit the script if an error happens
|
||||||
|
set -e
|
||||||
|
PUID=${PUID=0}
|
||||||
|
PGID=${PGID=0}
|
||||||
|
|
||||||
|
files_ownership () {
|
||||||
|
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
||||||
|
# -R Recursively descends the specified directories
|
||||||
|
# -c Like verbose but report only when a change is made
|
||||||
|
chown -hRc "$PUID":"$PGID" /app/data
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> Performing startup jobs and maintenance tasks"
|
||||||
|
files_ownership
|
||||||
|
|
||||||
|
echo "==> Starting application with user $PUID group $PGID"
|
||||||
|
|
||||||
|
# --clear-groups Clear supplementary groups.
|
||||||
|
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"
|
|
@ -6,12 +6,14 @@ const Database = require("../server/database");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
const { initJWTSecret } = require("../server/util-server");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Need to use es6 to read language files
|
// Need to use ES6 to read language files
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
@ -14,6 +14,7 @@ const copyRecursiveSync = function (src, dest) {
|
||||||
let exists = fs.existsSync(src);
|
let exists = fs.existsSync(src);
|
||||||
let stats = exists && fs.statSync(src);
|
let stats = exists && fs.statSync(src);
|
||||||
let isDirectory = exists && stats.isDirectory();
|
let isDirectory = exists && stats.isDirectory();
|
||||||
|
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
fs.mkdirSync(dest);
|
fs.mkdirSync(dest);
|
||||||
fs.readdirSync(src).forEach(function (childItemName) {
|
fs.readdirSync(src).forEach(function (childItemName) {
|
||||||
|
@ -24,8 +25,9 @@ const copyRecursiveSync = function (src, dest) {
|
||||||
fs.copyFileSync(src, dest);
|
fs.copyFileSync(src, dest);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
console.log(process.argv)
|
|
||||||
const baseLangCode = process.argv[2] || "zh-HK";
|
console.log("Arguments:", process.argv)
|
||||||
|
const baseLangCode = process.argv[2] || "en";
|
||||||
console.log("Base Lang: " + baseLangCode);
|
console.log("Base Lang: " + baseLangCode);
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
fs.rmdirSync("./languages", { recursive: true });
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
copyRecursiveSync("../../src/languages", "./languages");
|
||||||
|
@ -33,46 +35,50 @@ copyRecursiveSync("../../src/languages", "./languages");
|
||||||
const en = (await import("./languages/en.js")).default;
|
const en = (await import("./languages/en.js")).default;
|
||||||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
|
||||||
const files = fs.readdirSync("./languages");
|
const files = fs.readdirSync("./languages");
|
||||||
console.log(files);
|
console.log("Files:", files);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.endsWith(".js")) {
|
if (!file.endsWith(".js")) {
|
||||||
console.log("Processing " + file);
|
console.log("Skipping " + file)
|
||||||
const lang = await import("./languages/" + file);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let obj;
|
console.log("Processing " + file);
|
||||||
|
const lang = await import("./languages/" + file);
|
||||||
|
|
||||||
if (lang.default) {
|
let obj;
|
||||||
console.log("is js module");
|
|
||||||
obj = lang.default;
|
if (lang.default) {
|
||||||
} else {
|
obj = lang.default;
|
||||||
console.log("empty file");
|
} else {
|
||||||
obj = {
|
console.log("Empty file");
|
||||||
languageName: "<Your Language name in your language (not in English)>"
|
obj = {
|
||||||
};
|
languageName: "<Your Language name in your language (not in English)>"
|
||||||
}
|
};
|
||||||
|
}
|
||||||
// En first
|
|
||||||
for (const key in en) {
|
// En first
|
||||||
if (! obj[key]) {
|
for (const key in en) {
|
||||||
obj[key] = en[key];
|
if (! obj[key]) {
|
||||||
}
|
obj[key] = en[key];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseLang !== en) {
|
||||||
// Base second
|
// Base second
|
||||||
for (const key in baseLang) {
|
for (const key in baseLang) {
|
||||||
if (! obj[key]) {
|
if (! obj[key]) {
|
||||||
obj[key] = key;
|
obj[key] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = "export default " + util.inspect(obj, {
|
|
||||||
depth: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const code = "export default " + util.inspect(obj, {
|
||||||
|
depth: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(`../../src/languages/${file}`, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
fs.rmdirSync("./languages", { recursive: true });
|
||||||
console.log("Done, fix the format by eslint now");
|
console.log("Done. Fixing formatting by ESLint...");
|
||||||
|
|
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
|
|
@ -5,6 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta name="theme-color" id="theme-color" content="" />
|
<meta name="theme-color" id="theme-color" content="" />
|
||||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||||
<title>Uptime Kuma</title>
|
<title>Uptime Kuma</title>
|
||||||
|
|
6
jest-puppeteer.config.js
Normal file
6
jest-puppeteer.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
"launch": {
|
||||||
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,24 +8,25 @@ Kustomize is a tool which builds a complete deployment file for all config eleme
|
||||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
||||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
||||||
|
|
||||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
|
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
|
||||||
|
|
||||||
|
## What do I have to edit?
|
||||||
|
|
||||||
## What do i have to edit?
|
|
||||||
You have to edit the ```ingressroute.yml``` to your needs.
|
You have to edit the ```ingressroute.yml``` to your needs.
|
||||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
||||||
|
|
||||||
- host
|
- Host
|
||||||
- secrets and secret names
|
- Secrets and secret names
|
||||||
- (Cluster)Issuer (optional)
|
- (Cluster)Issuer (optional)
|
||||||
- the Version in the Deployment-File
|
- The Version in the Deployment-File
|
||||||
- update:
|
- Update:
|
||||||
- change to newer version and run the above commands, it will update the pods one after another
|
- Change to newer version and run the above commands, it will update the pods one after another
|
||||||
|
|
||||||
## How To use:
|
## How To use
|
||||||
|
|
||||||
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
||||||
- Edit files mentioned above to your needs
|
- Edit files mentioned above to your needs
|
||||||
- run ```kustomize build > apply.yml```
|
- Run ```kustomize build > apply.yml```
|
||||||
- run ```kubectl apply -f apply.yml```
|
- Run ```kubectl apply -f apply.yml```
|
||||||
|
|
||||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
||||||
|
|
10346
package-lock.json
generated
10346
package-lock.json
generated
File diff suppressed because it is too large
Load diff
150
package.json
150
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.6.0",
|
"version": "1.7.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -10,22 +10,30 @@
|
||||||
"node": "14.*"
|
"node": "14.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy-peer-deps": "npm install --legacy-peer-deps",
|
"install-legacy": "npm install --legacy-peer-deps",
|
||||||
"update-legacy-peer-deps": "npm update --legacy-peer-deps",
|
"update-legacy": "npm update --legacy-peer-deps",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||||
|
"jest": "node test/prepare-jest.js && jest",
|
||||||
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
"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.6.0-alpine --target release . --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": "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.6.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.6.0-debian --target release . --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 dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.3-alpine --target release . --push",
|
||||||
|
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.3-debian --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
|
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"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.6.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
|
"setup": "git checkout 1.7.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||||
"update-version": "node extra/update-version.js",
|
"update-version": "node extra/update-version.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
|
@ -34,65 +42,87 @@
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||||
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix"
|
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
"@fortawesome/vue-fontawesome": "~3.0.0-4",
|
||||||
"@popperjs/core": "^2.9.3",
|
"@louislam/sqlite3": "~5.0.6",
|
||||||
"args-parser": "^1.3.0",
|
"@popperjs/core": "~2.10.2",
|
||||||
"axios": "^0.21.1",
|
"args-parser": "~1.3.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"axios": "~0.21.4",
|
||||||
"bootstrap": "^5.1.0",
|
"bcryptjs": "~2.4.3",
|
||||||
"chart.js": "^3.5.1",
|
"bootstrap": "~5.1.1",
|
||||||
"chartjs-adapter-dayjs": "^1.0.0",
|
"chart.js": "~3.5.1",
|
||||||
"command-exists": "^1.2.9",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"compare-versions": "^3.6.0",
|
"command-exists": "~1.2.9",
|
||||||
"dayjs": "^1.10.6",
|
"compare-versions": "~3.6.0",
|
||||||
"express": "^4.17.1",
|
"dayjs": "~1.10.7",
|
||||||
"express-basic-auth": "^1.2.0",
|
"express": "~4.17.1",
|
||||||
"form-data": "^4.0.0",
|
"express-basic-auth": "~1.2.0",
|
||||||
"http-graceful-shutdown": "^3.1.4",
|
"form-data": "~4.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"http-graceful-shutdown": "~3.1.4",
|
||||||
"nodemailer": "^6.6.3",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"notp": "^2.0.3",
|
"nodemailer": "~6.6.5",
|
||||||
"password-hash": "^1.2.2",
|
"notp": "~2.0.3",
|
||||||
"prom-client": "^13.2.0",
|
"password-hash": "~1.2.2",
|
||||||
"prometheus-api-metrics": "^3.2.0",
|
"postcss-rtlcss": "~3.4.1",
|
||||||
"qrcode": "^1.4.4",
|
"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",
|
"redbean-node": "0.1.2",
|
||||||
"socket.io": "^4.2.0",
|
"socket.io": "~4.2.0",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "~4.2.0",
|
||||||
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
|
"tcp-ping": "~0.1.1",
|
||||||
"tcp-ping": "^0.1.1",
|
"thirty-two": "~1.0.2",
|
||||||
"thirty-two": "^1.0.2",
|
"timezones-list": "~3.0.1",
|
||||||
"v-pagination-3": "^0.1.6",
|
"v-pagination-3": "~0.1.6",
|
||||||
"vue": "^3.2.8",
|
"vue": "next",
|
||||||
"vue-chart-3": "^0.5.7",
|
"vue-chart-3": "~0.5.8",
|
||||||
"vue-confirm-dialog": "^1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-i18n": "^9.1.7",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-multiselect": "^3.0.0-alpha.2",
|
"vue-i18n": "~9.1.8",
|
||||||
"vue-qrcode": "^1.0.0",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-router": "^4.0.11",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-toastification": "^2.0.0-rc.1"
|
"vue-qrcode": "~1.0.0",
|
||||||
|
"vue-router": "~4.0.11",
|
||||||
|
"vue-toastification": "~2.0.0-rc.1",
|
||||||
|
"vuedraggable": "~4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.15.0",
|
"@babel/eslint-parser": "~7.15.7",
|
||||||
"@types/bootstrap": "^5.1.2",
|
"@types/bootstrap": "~5.1.6",
|
||||||
"@vitejs/plugin-legacy": "^1.5.2",
|
"@vitejs/plugin-legacy": "~1.5.3",
|
||||||
"@vitejs/plugin-vue": "^1.6.0",
|
"@vitejs/plugin-vue": "~1.9.2",
|
||||||
"@vue/compiler-sfc": "^3.2.6",
|
"@vue/compiler-sfc": "~3.2.19",
|
||||||
"core-js": "^3.17.0",
|
"core-js": "~3.18.1",
|
||||||
"dns2": "^2.0.1",
|
"cross-env": "~7.0.3",
|
||||||
"eslint": "^7.32.0",
|
"dns2": "~2.0.1",
|
||||||
"eslint-plugin-vue": "^7.17.0",
|
"eslint": "~7.32.0",
|
||||||
"sass": "^1.38.2",
|
"eslint-plugin-vue": "~7.18.0",
|
||||||
"stylelint": "^13.13.1",
|
"jest": "~27.2.4",
|
||||||
"stylelint-config-standard": "^22.0.0",
|
"jest-puppeteer": "~6.0.0",
|
||||||
"typescript": "^4.4.2",
|
"puppeteer": "~10.4.0",
|
||||||
"vite": "^2.5.3"
|
"sass": "~1.42.1",
|
||||||
|
"stylelint": "~13.13.1",
|
||||||
|
"stylelint-config-standard": "~22.0.0",
|
||||||
|
"typescript": "~4.4.3",
|
||||||
|
"vite": "~2.5.10"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"verbose": true,
|
||||||
|
"preset": "jest-puppeteer",
|
||||||
|
"globals": {
|
||||||
|
"__DEV__": true
|
||||||
|
},
|
||||||
|
"testRegex": "./test/*.spec.js",
|
||||||
|
"rootDir": ".",
|
||||||
|
"testTimeout": 15000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
public/icon-192x192.png
Normal file
BIN
public/icon-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon-512x512.png
Normal file
BIN
public/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
19
public/manifest.json
Normal file
19
public/manifest.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "Uptime Kuma",
|
||||||
|
"short_name": "Uptime Kuma",
|
||||||
|
"start_url": "/",
|
||||||
|
"background_color": "#fff",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -18,11 +18,10 @@ exports.startInterval = () => {
|
||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.version = "1000.0.0"
|
res.data.version = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latestVersion = res.data.version;
|
exports.latestVersion = res.data.version;
|
||||||
console.log("Latest Version: " + exports.latestVersion);
|
|
||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,11 +3,25 @@ const { R } = require("redbean-node");
|
||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const { debug, sleep } = require("../src/util");
|
const { debug, sleep } = require("../src/util");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
const knex = require("knex");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database & App Data Folder
|
||||||
|
*/
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
static templatePath = "./db/kuma.db";
|
static templatePath = "./db/kuma.db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Dir (Default: ./data)
|
||||||
|
*/
|
||||||
static dataDir;
|
static dataDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Upload Dir (Default: ./data/upload)
|
||||||
|
*/
|
||||||
|
static uploadDir;
|
||||||
|
|
||||||
static path;
|
static path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,39 +45,70 @@ class Database {
|
||||||
"patch-setting-value-type.sql": true,
|
"patch-setting-value-type.sql": true,
|
||||||
"patch-improve-performance.sql": true,
|
"patch-improve-performance.sql": true,
|
||||||
"patch-2fa.sql": true,
|
"patch-2fa.sql": true,
|
||||||
|
"patch-add-retry-interval-monitor.sql": true,
|
||||||
|
"patch-incident-table.sql": true,
|
||||||
|
"patch-group-table.sql": true,
|
||||||
|
"patch-monitor-push_token.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The finally version should be 10 after merged tag feature
|
* The finally version should be 10 after merged tag feature
|
||||||
* @deprecated Use patchList for any new feature
|
* @deprecated Use patchList for any new feature
|
||||||
*/
|
*/
|
||||||
static latestVersion = 9;
|
static latestVersion = 10;
|
||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
|
static init(args) {
|
||||||
|
// Data Directory (must be end with "/")
|
||||||
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
|
Database.path = Database.dataDir + "kuma.db";
|
||||||
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
|
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.uploadDir = Database.dataDir + "upload/";
|
||||||
|
|
||||||
|
if (! fs.existsSync(Database.uploadDir)) {
|
||||||
|
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Data Dir: ${Database.dataDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
static async connect() {
|
static async connect() {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
R.setup("sqlite", {
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
filename: Database.path,
|
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||||
|
|
||||||
|
const knexInstance = knex({
|
||||||
|
client: Dialect,
|
||||||
|
connection: {
|
||||||
|
filename: Database.path,
|
||||||
|
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||||
|
},
|
||||||
useNullAsDefault: true,
|
useNullAsDefault: true,
|
||||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
pool: {
|
||||||
}, {
|
min: 1,
|
||||||
min: 1,
|
max: 1,
|
||||||
max: 1,
|
idleTimeoutMillis: 120 * 1000,
|
||||||
idleTimeoutMillis: 120 * 1000,
|
propagateCreateError: false,
|
||||||
propagateCreateError: false,
|
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
R.setup(knexInstance);
|
||||||
|
|
||||||
if (process.env.SQL_LOG === "1") {
|
if (process.env.SQL_LOG === "1") {
|
||||||
R.debug(true);
|
R.debug(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true)
|
R.freeze(true);
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
|
await R.exec("PRAGMA foreign_keys = ON");
|
||||||
// Change to WAL
|
// Change to WAL
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
|
@ -71,6 +116,7 @@ class Database {
|
||||||
console.log("SQLite config:");
|
console.log("SQLite config:");
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
console.log(await R.getAll("PRAGMA cache_size"));
|
||||||
|
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
|
@ -88,7 +134,7 @@ class Database {
|
||||||
} else if (version > this.latestVersion) {
|
} else if (version > this.latestVersion) {
|
||||||
console.info("Warning: Database version is newer than expected");
|
console.info("Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
console.info("Database patch is needed")
|
console.info("Database patch is needed");
|
||||||
|
|
||||||
this.backup(version);
|
this.backup(version);
|
||||||
|
|
||||||
|
@ -103,11 +149,12 @@ class Database {
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
this.restore();
|
|
||||||
|
|
||||||
console.error(ex)
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +179,7 @@ class Database {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let sqlFilename in this.patchList) {
|
for (let sqlFilename in this.patchList) {
|
||||||
await this.patch2Recursion(sqlFilename, databasePatchedFiles)
|
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.patched) {
|
if (this.patched) {
|
||||||
|
@ -141,11 +188,13 @@ class Database {
|
||||||
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
this.restore();
|
|
||||||
|
|
||||||
console.error(ex)
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +234,7 @@ class Database {
|
||||||
console.log(sqlFilename + " is patched successfully");
|
console.log(sqlFilename + " is patched successfully");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log(sqlFilename + " is already patched, skip");
|
debug(sqlFilename + " is already patched, skip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,12 +252,12 @@ class Database {
|
||||||
// Remove all comments (--)
|
// Remove all comments (--)
|
||||||
let lines = text.split("\n");
|
let lines = text.split("\n");
|
||||||
lines = lines.filter((line) => {
|
lines = lines.filter((line) => {
|
||||||
return ! line.startsWith("--")
|
return ! line.startsWith("--");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Split statements by semicolon
|
// Split statements by semicolon
|
||||||
// Filter out empty line
|
// Filter out empty line
|
||||||
text = lines.join("\n")
|
text = lines.join("\n");
|
||||||
|
|
||||||
let statements = text.split(";")
|
let statements = text.split(";")
|
||||||
.map((statement) => {
|
.map((statement) => {
|
||||||
|
@ -216,7 +265,7 @@ class Database {
|
||||||
})
|
})
|
||||||
.filter((statement) => {
|
.filter((statement) => {
|
||||||
return statement !== "";
|
return statement !== "";
|
||||||
})
|
});
|
||||||
|
|
||||||
for (let statement of statements) {
|
for (let statement of statements) {
|
||||||
await R.exec(statement);
|
await R.exec(statement);
|
||||||
|
@ -262,7 +311,7 @@ class Database {
|
||||||
*/
|
*/
|
||||||
static backup(version) {
|
static backup(version) {
|
||||||
if (! this.backupPath) {
|
if (! this.backupPath) {
|
||||||
console.info("Backup the db")
|
console.info("Backup the db");
|
||||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
fs.copyFileSync(Database.path, this.backupPath);
|
fs.copyFileSync(Database.path, this.backupPath);
|
||||||
|
|
||||||
|
|
57
server/image-data-uri.js
Normal file
57
server/image-data-uri.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
||||||
|
Modified with 0 dependencies
|
||||||
|
*/
|
||||||
|
let fs = require("fs");
|
||||||
|
|
||||||
|
let ImageDataURI = (() => {
|
||||||
|
|
||||||
|
function decode(dataURI) {
|
||||||
|
if (!/data:image\//.test(dataURI)) {
|
||||||
|
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)");
|
||||||
|
return {
|
||||||
|
imageType: regExMatches[1],
|
||||||
|
dataBase64: regExMatches[2],
|
||||||
|
dataBuffer: new Buffer(regExMatches[2], "base64")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(data, mediaType) {
|
||||||
|
if (!data || !mediaType) {
|
||||||
|
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType;
|
||||||
|
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64");
|
||||||
|
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64;
|
||||||
|
|
||||||
|
return dataImgBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function outputFile(dataURI, filePath) {
|
||||||
|
filePath = filePath || "./";
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let imageDecoded = decode(dataURI);
|
||||||
|
|
||||||
|
fs.writeFile(filePath, imageDecoded.dataBuffer, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4));
|
||||||
|
}
|
||||||
|
resolve(filePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decode: decode,
|
||||||
|
encode: encode,
|
||||||
|
outputFile: outputFile,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
module.exports = ImageDataURI;
|
34
server/model/group.js
Normal file
34
server/model/group.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class Group extends BeanModel {
|
||||||
|
|
||||||
|
async toPublicJSON() {
|
||||||
|
let monitorBeanList = await this.getMonitorList();
|
||||||
|
let monitorList = [];
|
||||||
|
|
||||||
|
for (let bean of monitorBeanList) {
|
||||||
|
monitorList.push(await bean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
weight: this.weight,
|
||||||
|
monitorList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMonitorList() {
|
||||||
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
|
SELECT monitor.* FROM monitor, monitor_group
|
||||||
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
|
AND group_id = ?
|
||||||
|
ORDER BY monitor_group.weight
|
||||||
|
`, [
|
||||||
|
this.id,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Group;
|
|
@ -1,8 +1,8 @@
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
toPublicJSON() {
|
||||||
|
return {
|
||||||
|
status: this.status,
|
||||||
|
time: this.time,
|
||||||
|
msg: "", // Hide for public
|
||||||
|
ping: this.ping,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this.monitor_id,
|
||||||
|
|
18
server/model/incident.js
Normal file
18
server/model/incident.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Incident extends BeanModel {
|
||||||
|
|
||||||
|
toPublicJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
style: this.style,
|
||||||
|
title: this.title,
|
||||||
|
content: this.content,
|
||||||
|
pin: this.pin,
|
||||||
|
createdDate: this.createdDate,
|
||||||
|
lastUpdatedDate: this.lastUpdatedDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Incident;
|
|
@ -1,16 +1,16 @@
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification")
|
const { Notification } = require("../notification");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,18 +20,35 @@ const version = require("../../package.json").version;
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON
|
||||||
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
let notificationIDList = {};
|
let notificationIDList = {};
|
||||||
|
|
||||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
@ -43,6 +60,7 @@ class Monitor extends BeanModel {
|
||||||
active: this.active,
|
active: this.active,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
|
retryInterval: this.retryInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
|
@ -51,7 +69,9 @@ class Monitor extends BeanModel {
|
||||||
dns_resolve_type: this.dns_resolve_type,
|
dns_resolve_type: this.dns_resolve_type,
|
||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
|
pushToken: this.pushToken,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +80,7 @@ class Monitor extends BeanModel {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
getIgnoreTls() {
|
getIgnoreTls() {
|
||||||
return Boolean(this.ignoreTls)
|
return Boolean(this.ignoreTls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,12 +110,12 @@ class Monitor extends BeanModel {
|
||||||
if (! previousBeat) {
|
if (! previousBeat) {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
const isFirstBeat = !previousBeat;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat")
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
@ -131,7 +151,7 @@ class Monitor extends BeanModel {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
bean.msg = `${res.status} - ${res.statusText}`
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
|
@ -141,12 +161,12 @@ class Monitor extends BeanModel {
|
||||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "No TLS certificate in response") {
|
if (e.message !== "No TLS certificate in response") {
|
||||||
console.error(e.message)
|
console.error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
@ -156,26 +176,26 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
// Convert to string for object/array
|
// Convert to string for object/array
|
||||||
if (typeof data !== "string") {
|
if (typeof data !== "string") {
|
||||||
data = JSON.stringify(data)
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
if (data.includes(this.keyword)) {
|
||||||
bean.msg += ", keyword is found"
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found")
|
throw new Error(bean.msg + ", but keyword is not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
bean.ping = await tcping(this.hostname, this.port);
|
bean.ping = await tcping(this.hostname, this.port);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
|
||||||
} else if (this.type === "ping") {
|
} else if (this.type === "ping") {
|
||||||
bean.ping = await ping(this.hostname);
|
bean.ping = await ping(this.hostname);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "dns") {
|
} else if (this.type === "dns") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
@ -195,7 +215,7 @@ class Monitor extends BeanModel {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2)
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
} else if (this.dns_resolve_type == "NS") {
|
} else if (this.dns_resolve_type == "NS") {
|
||||||
dnsMessage += "Servers: ";
|
dnsMessage += "Servers: ";
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage += dnsRes.join(" | ");
|
||||||
|
@ -205,7 +225,7 @@ class Monitor extends BeanModel {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2)
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dnsLastResult !== dnsMessage) {
|
if (this.dnsLastResult !== dnsMessage) {
|
||||||
|
@ -217,6 +237,28 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
bean.msg = dnsMessage;
|
bean.msg = dnsMessage;
|
||||||
bean.status = UP;
|
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 {
|
||||||
|
bean.msg = "Unknown Monitor Type";
|
||||||
|
bean.status = PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
if (this.isUpsideDown()) {
|
||||||
|
@ -244,6 +286,8 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
// UP -> PENDING = not important
|
// UP -> PENDING = not important
|
||||||
// * UP -> DOWN = important
|
// * UP -> DOWN = important
|
||||||
|
@ -268,20 +312,20 @@ class Monitor extends BeanModel {
|
||||||
if (!isFirstBeat || bean.status === 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 ", [
|
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
text = "✅ Up"
|
text = "✅ Up";
|
||||||
} else {
|
} else {
|
||||||
text = "🔴 Down"
|
text = "🔴 Down";
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Cannot send notification to " + notification.name);
|
console.error("Cannot send notification to " + notification.name);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
@ -294,15 +338,18 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else if (bean.status === PENDING) {
|
} else if (bean.status === PENDING) {
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
|
if (this.retryInterval > 0) {
|
||||||
|
beatInterval = this.retryInterval;
|
||||||
|
}
|
||||||
|
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
Monitor.sendStats(io, this.id, this.user_id)
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
@ -310,12 +357,19 @@ class Monitor extends BeanModel {
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
beat();
|
// Delay Push Type
|
||||||
|
if (this.type === "push") {
|
||||||
|
setTimeout(() => {
|
||||||
|
beat();
|
||||||
|
}, this.interval * 1000);
|
||||||
|
} else {
|
||||||
|
beat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -406,7 +460,7 @@ class Monitor extends BeanModel {
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||||
* @param duration : int Hours
|
* @param duration : int Hours
|
||||||
*/
|
*/
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async calcUptime(duration, monitorID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
||||||
|
@ -459,12 +513,21 @@ class Monitor extends BeanModel {
|
||||||
} else {
|
} else {
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||||
console.log("here???" + status);
|
|
||||||
if (status === UP) {
|
if (status === UP) {
|
||||||
uptime = 1;
|
uptime = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Uptime
|
||||||
|
* @param duration : int Hours
|
||||||
|
*/
|
||||||
|
static async sendUptime(duration, io, monitorID, userID) {
|
||||||
|
const uptime = await this.calcUptime(duration, monitorID);
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
server/model/tag.js
Normal file
13
server/model/tag.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Tag extends BeanModel {
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this._id,
|
||||||
|
name: this._name,
|
||||||
|
color: this._color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Tag;
|
749
server/modules/apicache/apicache.js
Normal file
749
server/modules/apicache/apicache.js
Normal file
|
@ -0,0 +1,749 @@
|
||||||
|
let url = require("url");
|
||||||
|
let MemoryCache = require("./memory-cache");
|
||||||
|
|
||||||
|
let t = {
|
||||||
|
ms: 1,
|
||||||
|
second: 1000,
|
||||||
|
minute: 60000,
|
||||||
|
hour: 3600000,
|
||||||
|
day: 3600000 * 24,
|
||||||
|
week: 3600000 * 24 * 7,
|
||||||
|
month: 3600000 * 24 * 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let instances = [];
|
||||||
|
|
||||||
|
let matches = function (a) {
|
||||||
|
return function (b) {
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let doesntMatch = function (a) {
|
||||||
|
return function (b) {
|
||||||
|
return !matches(a)(b);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let logDuration = function (d, prefix) {
|
||||||
|
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||||
|
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSafeHeaders(res) {
|
||||||
|
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiCache() {
|
||||||
|
let memCache = new MemoryCache();
|
||||||
|
|
||||||
|
let globalOptions = {
|
||||||
|
debug: false,
|
||||||
|
defaultDuration: 3600000,
|
||||||
|
enabled: true,
|
||||||
|
appendKey: [],
|
||||||
|
jsonp: false,
|
||||||
|
redisClient: false,
|
||||||
|
headerBlacklist: [],
|
||||||
|
statusCodes: {
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
expire: undefined,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
// 'cache-control': 'no-cache' // example of header overwrite
|
||||||
|
},
|
||||||
|
trackPerformance: false,
|
||||||
|
respectCacheControl: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let middlewareOptions = [];
|
||||||
|
let instance = this;
|
||||||
|
let index = null;
|
||||||
|
let timers = {};
|
||||||
|
let performanceArray = []; // for tracking cache hit rate
|
||||||
|
|
||||||
|
instances.push(this);
|
||||||
|
this.id = instances.length;
|
||||||
|
|
||||||
|
function debug(a, b, c, d) {
|
||||||
|
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
||||||
|
return arg !== undefined;
|
||||||
|
});
|
||||||
|
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
|
||||||
|
|
||||||
|
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCacheResponse(request, response, toggle) {
|
||||||
|
let opt = globalOptions;
|
||||||
|
let codes = opt.statusCodes;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggle && !toggle(request, response)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIndexEntries(key, req) {
|
||||||
|
let groupName = req.apicacheGroup;
|
||||||
|
|
||||||
|
if (groupName) {
|
||||||
|
debug("group detected \"" + groupName + "\"");
|
||||||
|
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
||||||
|
group.unshift(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
index.all.unshift(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBlacklistedHeaders(headers) {
|
||||||
|
return Object.keys(headers)
|
||||||
|
.filter(function (key) {
|
||||||
|
return globalOptions.headerBlacklist.indexOf(key) === -1;
|
||||||
|
})
|
||||||
|
.reduce(function (acc, header) {
|
||||||
|
acc[header] = headers[header];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCacheObject(status, headers, data, encoding) {
|
||||||
|
return {
|
||||||
|
status: status,
|
||||||
|
headers: filterBlacklistedHeaders(headers),
|
||||||
|
data: data,
|
||||||
|
encoding: encoding,
|
||||||
|
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheResponse(key, value, duration) {
|
||||||
|
let redis = globalOptions.redisClient;
|
||||||
|
let expireCallback = globalOptions.events.expire;
|
||||||
|
|
||||||
|
if (redis && redis.connected) {
|
||||||
|
try {
|
||||||
|
redis.hset(key, "response", JSON.stringify(value));
|
||||||
|
redis.hset(key, "duration", duration);
|
||||||
|
redis.expire(key, duration / 1000, expireCallback || function () {});
|
||||||
|
} catch (err) {
|
||||||
|
debug("[apicache] error in redis.hset()");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memCache.add(key, value, duration, expireCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add automatic cache clearing from duration, includes max limit on setTimeout
|
||||||
|
timers[key] = setTimeout(function () {
|
||||||
|
instance.clear(key, true);
|
||||||
|
}, Math.min(duration, 2147483647));
|
||||||
|
}
|
||||||
|
|
||||||
|
function accumulateContent(res, content) {
|
||||||
|
if (content) {
|
||||||
|
if (typeof content == "string") {
|
||||||
|
res._apicache.content = (res._apicache.content || "") + content;
|
||||||
|
} else if (Buffer.isBuffer(content)) {
|
||||||
|
let oldContent = res._apicache.content;
|
||||||
|
|
||||||
|
if (typeof oldContent === "string") {
|
||||||
|
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldContent) {
|
||||||
|
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
res._apicache.content = Buffer.concat(
|
||||||
|
[oldContent, content],
|
||||||
|
oldContent.length + content.length
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res._apicache.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||||
|
// monkeypatch res.end to create cache object
|
||||||
|
res._apicache = {
|
||||||
|
write: res.write,
|
||||||
|
writeHead: res.writeHead,
|
||||||
|
end: res.end,
|
||||||
|
cacheable: true,
|
||||||
|
content: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// append header overwrites if applicable
|
||||||
|
Object.keys(globalOptions.headers).forEach(function (name) {
|
||||||
|
res.setHeader(name, globalOptions.headers[name]);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead = function () {
|
||||||
|
// add cache control headers
|
||||||
|
if (!globalOptions.headers["cache-control"]) {
|
||||||
|
if (shouldCacheResponse(req, res, toggle)) {
|
||||||
|
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
|
||||||
|
} else {
|
||||||
|
res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
|
||||||
|
return res._apicache.writeHead.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// patch res.write
|
||||||
|
res.write = function (content) {
|
||||||
|
accumulateContent(res, content);
|
||||||
|
return res._apicache.write.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// patch res.end
|
||||||
|
res.end = function (content, encoding) {
|
||||||
|
if (shouldCacheResponse(req, res, toggle)) {
|
||||||
|
accumulateContent(res, content);
|
||||||
|
|
||||||
|
if (res._apicache.cacheable && res._apicache.content) {
|
||||||
|
addIndexEntries(key, req);
|
||||||
|
let headers = res._apicache.headers || getSafeHeaders(res);
|
||||||
|
let cacheObject = createCacheObject(
|
||||||
|
res.statusCode,
|
||||||
|
headers,
|
||||||
|
res._apicache.content,
|
||||||
|
encoding
|
||||||
|
);
|
||||||
|
cacheResponse(key, cacheObject, duration);
|
||||||
|
|
||||||
|
// display log entry
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
|
||||||
|
debug("_apicache.headers: ", res._apicache.headers);
|
||||||
|
debug("res.getHeaders(): ", getSafeHeaders(res));
|
||||||
|
debug("cacheObject: ", cacheObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res._apicache.end.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||||
|
if (toggle && !toggle(request, response)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = getSafeHeaders(response);
|
||||||
|
|
||||||
|
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
||||||
|
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
||||||
|
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
|
||||||
|
|
||||||
|
// only embed apicache headers when not in production environment
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
Object.assign(headers, {
|
||||||
|
"apicache-store": globalOptions.redisClient ? "redis" : "memory",
|
||||||
|
"apicache-version": "1.6.2-modified",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// unstringify buffers
|
||||||
|
let data = cacheObject.data;
|
||||||
|
if (data && data.type === "Buffer") {
|
||||||
|
data =
|
||||||
|
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test Etag against If-None-Match for 304
|
||||||
|
let cachedEtag = cacheObject.headers.etag;
|
||||||
|
let requestEtag = request.headers["if-none-match"];
|
||||||
|
|
||||||
|
if (requestEtag && cachedEtag === requestEtag) {
|
||||||
|
response.writeHead(304, headers);
|
||||||
|
return response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.writeHead(cacheObject.status || 200, headers);
|
||||||
|
|
||||||
|
return response.end(data, cacheObject.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOptions() {
|
||||||
|
for (let i in middlewareOptions) {
|
||||||
|
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear = function (target, isAutomatic) {
|
||||||
|
let group = index.groups[target];
|
||||||
|
let redis = globalOptions.redisClient;
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
debug("clearing group \"" + target + "\"");
|
||||||
|
|
||||||
|
group.forEach(function (key) {
|
||||||
|
debug("clearing cached entry for \"" + key + "\"");
|
||||||
|
clearTimeout(timers[key]);
|
||||||
|
delete timers[key];
|
||||||
|
if (!globalOptions.redisClient) {
|
||||||
|
memCache.delete(key);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
redis.del(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.all = index.all.filter(doesntMatch(key));
|
||||||
|
});
|
||||||
|
|
||||||
|
delete index.groups[target];
|
||||||
|
} else if (target) {
|
||||||
|
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
|
||||||
|
clearTimeout(timers[target]);
|
||||||
|
delete timers[target];
|
||||||
|
// clear actual cached entry
|
||||||
|
if (!redis) {
|
||||||
|
memCache.delete(target);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
redis.del(target);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + target + "\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove from global index
|
||||||
|
index.all = index.all.filter(doesntMatch(target));
|
||||||
|
|
||||||
|
// remove target from each group that it may exist in
|
||||||
|
Object.keys(index.groups).forEach(function (groupName) {
|
||||||
|
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
|
||||||
|
|
||||||
|
// delete group if now empty
|
||||||
|
if (!index.groups[groupName].length) {
|
||||||
|
delete index.groups[groupName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
debug("clearing entire index");
|
||||||
|
|
||||||
|
if (!redis) {
|
||||||
|
memCache.clear();
|
||||||
|
} else {
|
||||||
|
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
||||||
|
index.all.forEach(function (key) {
|
||||||
|
clearTimeout(timers[key]);
|
||||||
|
delete timers[key];
|
||||||
|
try {
|
||||||
|
redis.del(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.resetIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseDuration(duration, defaultDuration) {
|
||||||
|
if (typeof duration === "number") {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof duration === "string") {
|
||||||
|
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
|
||||||
|
|
||||||
|
if (split.length === 3) {
|
||||||
|
let len = parseFloat(split[1]);
|
||||||
|
let unit = split[2].replace(/s$/i, "").toLowerCase();
|
||||||
|
if (unit === "m") {
|
||||||
|
unit = "ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (len || 1) * (t[unit] || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getDuration = function (duration) {
|
||||||
|
return parseDuration(duration, globalOptions.defaultDuration);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
||||||
|
* <code>
|
||||||
|
* app.get('/api/cache/performance', (req, res) => {
|
||||||
|
* res.json(apicache.getPerformance())
|
||||||
|
* })
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
this.getPerformance = function () {
|
||||||
|
return performanceArray.map(function (p) {
|
||||||
|
return p.report();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getIndex = function (group) {
|
||||||
|
if (group) {
|
||||||
|
return index.groups[group];
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||||
|
let duration = instance.getDuration(strDuration);
|
||||||
|
let opt = {};
|
||||||
|
|
||||||
|
middlewareOptions.push({
|
||||||
|
options: opt,
|
||||||
|
});
|
||||||
|
|
||||||
|
let options = function (localOptions) {
|
||||||
|
if (localOptions) {
|
||||||
|
middlewareOptions.find(function (middleware) {
|
||||||
|
return middleware.options === opt;
|
||||||
|
}).localOptions = localOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncOptions();
|
||||||
|
|
||||||
|
return opt;
|
||||||
|
};
|
||||||
|
|
||||||
|
options(localOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Function for non tracking performance
|
||||||
|
*/
|
||||||
|
function NOOPCachePerformance() {
|
||||||
|
this.report = this.hit = this.miss = function () {}; // noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
||||||
|
*/
|
||||||
|
function CachePerformance() {
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 100 requests.
|
||||||
|
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 1000 requests.
|
||||||
|
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 10000 requests.
|
||||||
|
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 100000 requests.
|
||||||
|
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of calls that have passed through the middleware since the server started.
|
||||||
|
*/
|
||||||
|
this.callCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of hits since the server started
|
||||||
|
*/
|
||||||
|
this.hitCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
|
this.lastCacheHit = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
|
this.lastCacheMiss = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return performance statistics
|
||||||
|
*/
|
||||||
|
this.report = function () {
|
||||||
|
return {
|
||||||
|
lastCacheHit: this.lastCacheHit,
|
||||||
|
lastCacheMiss: this.lastCacheMiss,
|
||||||
|
callCount: this.callCount,
|
||||||
|
hitCount: this.hitCount,
|
||||||
|
missCount: this.callCount - this.hitCount,
|
||||||
|
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
|
||||||
|
hitRateLast100: this.hitRate(this.hitsLast100),
|
||||||
|
hitRateLast1000: this.hitRate(this.hitsLast1000),
|
||||||
|
hitRateLast10000: this.hitRate(this.hitsLast10000),
|
||||||
|
hitRateLast100000: this.hitRate(this.hitsLast100000),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a cache hit rate from an array of hits and misses.
|
||||||
|
* @param {Uint8Array} array An array representing hits and misses.
|
||||||
|
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
||||||
|
*/
|
||||||
|
this.hitRate = function (array) {
|
||||||
|
let hits = 0;
|
||||||
|
let misses = 0;
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
let n8 = array[i];
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
switch (n8 & 3) {
|
||||||
|
case 1:
|
||||||
|
hits++;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
misses++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
n8 >>= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let total = hits + misses;
|
||||||
|
if (total == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return hits / total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a hit or miss in the given array. It will be recorded at a position determined
|
||||||
|
* by the current value of the callCount variable.
|
||||||
|
* @param {Uint8Array} array An array representing hits and misses.
|
||||||
|
* @param {boolean} hit true for a hit, false for a miss
|
||||||
|
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
||||||
|
* Each hit or miss is encoded as to bits as follows:
|
||||||
|
* 00 means no hit or miss has been recorded in these bits
|
||||||
|
* 01 encodes a hit
|
||||||
|
* 10 encodes a miss
|
||||||
|
*/
|
||||||
|
this.recordHitInArray = function (array, hit) {
|
||||||
|
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||||
|
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||||
|
let clearMask = ~(3 << bitOffset);
|
||||||
|
let record = (hit ? 1 : 2) << bitOffset;
|
||||||
|
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the hit or miss in the tracking arrays and increments the call count.
|
||||||
|
* @param {boolean} hit true records a hit, false records a miss
|
||||||
|
*/
|
||||||
|
this.recordHit = function (hit) {
|
||||||
|
this.recordHitInArray(this.hitsLast100, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast1000, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast10000, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast100000, hit);
|
||||||
|
if (hit) {
|
||||||
|
this.hitCount++;
|
||||||
|
}
|
||||||
|
this.callCount++;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a hit event, setting lastCacheMiss to the given key
|
||||||
|
* @param {string} key The key that had the cache hit
|
||||||
|
*/
|
||||||
|
this.hit = function (key) {
|
||||||
|
this.recordHit(true);
|
||||||
|
this.lastCacheHit = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a miss event, setting lastCacheMiss to the given key
|
||||||
|
* @param {string} key The key that had the cache miss
|
||||||
|
*/
|
||||||
|
this.miss = function (key) {
|
||||||
|
this.recordHit(false);
|
||||||
|
this.lastCacheMiss = key;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
|
||||||
|
|
||||||
|
performanceArray.push(perf);
|
||||||
|
|
||||||
|
let cache = function (req, res, next) {
|
||||||
|
function bypass() {
|
||||||
|
debug("bypass detected, skipping cache.");
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial bypass chances
|
||||||
|
if (!opt.enabled) {
|
||||||
|
return bypass();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.headers["x-apicache-bypass"] ||
|
||||||
|
req.headers["x-apicache-force-fetch"] ||
|
||||||
|
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
|
||||||
|
) {
|
||||||
|
return bypass();
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
||||||
|
// if (typeof middlewareToggle === 'function') {
|
||||||
|
// if (!middlewareToggle(req, res)) return bypass()
|
||||||
|
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
||||||
|
// return bypass()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// embed timer
|
||||||
|
req.apicacheTimer = new Date();
|
||||||
|
|
||||||
|
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
||||||
|
let key = req.originalUrl || req.url;
|
||||||
|
|
||||||
|
// Remove querystring from key if jsonp option is enabled
|
||||||
|
if (opt.jsonp) {
|
||||||
|
key = url.parse(key).pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add appendKey (either custom function or response path)
|
||||||
|
if (typeof opt.appendKey === "function") {
|
||||||
|
key += "$$appendKey=" + opt.appendKey(req, res);
|
||||||
|
} else if (opt.appendKey.length > 0) {
|
||||||
|
let appendKey = req;
|
||||||
|
|
||||||
|
for (let i = 0; i < opt.appendKey.length; i++) {
|
||||||
|
appendKey = appendKey[opt.appendKey[i]];
|
||||||
|
}
|
||||||
|
key += "$$appendKey=" + appendKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt cache hit
|
||||||
|
let redis = opt.redisClient;
|
||||||
|
let cached = !redis ? memCache.getValue(key) : null;
|
||||||
|
|
||||||
|
// send if cache hit from memory-cache
|
||||||
|
if (cached) {
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
|
||||||
|
|
||||||
|
perf.hit(key);
|
||||||
|
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send if cache hit from redis
|
||||||
|
if (redis && redis.connected) {
|
||||||
|
try {
|
||||||
|
redis.hgetall(key, function (err, obj) {
|
||||||
|
if (!err && obj && obj.response) {
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("sending cached (redis) version of", key, logDuration(elapsed));
|
||||||
|
|
||||||
|
perf.hit(key);
|
||||||
|
return sendCachedResponse(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
JSON.parse(obj.response),
|
||||||
|
middlewareToggle,
|
||||||
|
next,
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next,
|
||||||
|
key,
|
||||||
|
duration,
|
||||||
|
strDuration,
|
||||||
|
middlewareToggle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// bypass redis on error
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.options = options;
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.options = function (options) {
|
||||||
|
if (options) {
|
||||||
|
Object.assign(globalOptions, options);
|
||||||
|
syncOptions();
|
||||||
|
|
||||||
|
if ("defaultDuration" in options) {
|
||||||
|
// Convert the default duration to a number in milliseconds (if needed)
|
||||||
|
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalOptions.trackPerformance) {
|
||||||
|
debug("WARNING: using trackPerformance flag can cause high memory usage!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
} else {
|
||||||
|
return globalOptions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resetIndex = function () {
|
||||||
|
index = {
|
||||||
|
all: [],
|
||||||
|
groups: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newInstance = function (config) {
|
||||||
|
let instance = new ApiCache();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
instance.options(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clone = function () {
|
||||||
|
return this.newInstance(this.options());
|
||||||
|
};
|
||||||
|
|
||||||
|
// initialize index
|
||||||
|
this.resetIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ApiCache();
|
14
server/modules/apicache/index.js
Normal file
14
server/modules/apicache/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const apicache = require("./apicache");
|
||||||
|
|
||||||
|
apicache.options({
|
||||||
|
headerBlacklist: [
|
||||||
|
"cache-control"
|
||||||
|
],
|
||||||
|
headers: {
|
||||||
|
// Disable client side cache, only server side cache.
|
||||||
|
// BUG! Not working for the second request
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = apicache;
|
59
server/modules/apicache/memory-cache.js
Normal file
59
server/modules/apicache/memory-cache.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
function MemoryCache() {
|
||||||
|
this.cache = {};
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||||
|
let old = this.cache[key];
|
||||||
|
let instance = this;
|
||||||
|
|
||||||
|
let entry = {
|
||||||
|
value: value,
|
||||||
|
expire: time + Date.now(),
|
||||||
|
timeout: setTimeout(function () {
|
||||||
|
instance.delete(key);
|
||||||
|
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key);
|
||||||
|
}, time)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cache[key] = entry;
|
||||||
|
this.size = Object.keys(this.cache).length;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.delete = function (key) {
|
||||||
|
let entry = this.cache[key];
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
clearTimeout(entry.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.cache[key];
|
||||||
|
|
||||||
|
this.size = Object.keys(this.cache).length;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.get = function (key) {
|
||||||
|
let entry = this.cache[key];
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.getValue = function (key) {
|
||||||
|
let entry = this.get(key);
|
||||||
|
|
||||||
|
return entry && entry.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.clear = function () {
|
||||||
|
Object.keys(this.cache).forEach(function (key) {
|
||||||
|
this.delete(key);
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MemoryCache;
|
|
@ -62,6 +62,11 @@ class Discord extends NotificationProvider {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notification.discordPrefixMessage) {
|
||||||
|
discorddowndata.content = notification.discordPrefixMessage;
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
||||||
|
@ -92,6 +97,11 @@ class Discord extends NotificationProvider {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notification.discordPrefixMessage) {
|
||||||
|
discordupdata.content = notification.discordPrefixMessage;
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(notification.discordWebhookUrl, discordupdata)
|
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
124
server/notification-providers/teams.js
Normal file
124
server/notification-providers/teams.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class Teams extends NotificationProvider {
|
||||||
|
name = "teams";
|
||||||
|
|
||||||
|
_statusMessageFactory = (status, monitorName) => {
|
||||||
|
if (status === DOWN) {
|
||||||
|
return `🔴 Application [${monitorName}] went down`;
|
||||||
|
} else if (status === UP) {
|
||||||
|
return `✅ Application [${monitorName}] is back online`;
|
||||||
|
}
|
||||||
|
return "Notification";
|
||||||
|
};
|
||||||
|
|
||||||
|
_getThemeColor = (status) => {
|
||||||
|
if (status === DOWN) {
|
||||||
|
return "ff0000";
|
||||||
|
}
|
||||||
|
if (status === UP) {
|
||||||
|
return "00e804";
|
||||||
|
}
|
||||||
|
return "008cff";
|
||||||
|
};
|
||||||
|
|
||||||
|
_notificationPayloadFactory = ({
|
||||||
|
status,
|
||||||
|
monitorMessage,
|
||||||
|
monitorName,
|
||||||
|
monitorUrl,
|
||||||
|
}) => {
|
||||||
|
const notificationMessage = this._statusMessageFactory(
|
||||||
|
status,
|
||||||
|
monitorName
|
||||||
|
);
|
||||||
|
|
||||||
|
const facts = [];
|
||||||
|
|
||||||
|
if (monitorName) {
|
||||||
|
facts.push({
|
||||||
|
name: "Monitor",
|
||||||
|
value: monitorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitorUrl) {
|
||||||
|
facts.push({
|
||||||
|
name: "URL",
|
||||||
|
value: monitorUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org/extensions",
|
||||||
|
"@type": "MessageCard",
|
||||||
|
themeColor: this._getThemeColor(status),
|
||||||
|
summary: notificationMessage,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
activityImage:
|
||||||
|
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||||
|
activityTitle: "**Uptime Kuma**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activityTitle: notificationMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activityTitle: "**Description**",
|
||||||
|
text: monitorMessage,
|
||||||
|
facts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_sendNotification = async (webhookUrl, payload) => {
|
||||||
|
await axios.post(webhookUrl, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._sendNotification(webhookUrl, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully. ";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
await this._handleGeneralNotification(notification.webhookUrl, msg);
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (monitorJSON["type"] === "port") {
|
||||||
|
url = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
url += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = monitorJSON["url"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: heartbeatJSON.msg,
|
||||||
|
monitorName: monitorJSON.name,
|
||||||
|
monitorUrl: url,
|
||||||
|
status: heartbeatJSON.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._sendNotification(notification.webhookUrl, payload);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Teams;
|
|
@ -13,6 +13,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
|
||||||
const Signal = require("./notification-providers/signal");
|
const Signal = require("./notification-providers/signal");
|
||||||
const Slack = require("./notification-providers/slack");
|
const Slack = require("./notification-providers/slack");
|
||||||
const SMTP = require("./notification-providers/smtp");
|
const SMTP = require("./notification-providers/smtp");
|
||||||
|
const Teams = require("./notification-providers/teams");
|
||||||
const Telegram = require("./notification-providers/telegram");
|
const Telegram = require("./notification-providers/telegram");
|
||||||
const Webhook = require("./notification-providers/webhook");
|
const Webhook = require("./notification-providers/webhook");
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ class Notification {
|
||||||
const list = [
|
const list = [
|
||||||
new Apprise(),
|
new Apprise(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
|
new Teams(),
|
||||||
new Gotify(),
|
new Gotify(),
|
||||||
new Line(),
|
new Line(),
|
||||||
new LunaSea(),
|
new LunaSea(),
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Prometheus {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
|
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
191
server/routers/api-router.js
Normal file
191
server/routers/api-router.js
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
let express = require("express");
|
||||||
|
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const server = require("../server");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const Monitor = require("../model/monitor");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { UP } = 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;
|
||||||
|
|
||||||
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
|
pushToken
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! monitor) {
|
||||||
|
throw new Error("Monitor not found or not active.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bean = R.dispense("heartbeat");
|
||||||
|
bean.monitor_id = monitor.id;
|
||||||
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = msg;
|
||||||
|
bean.ping = ping;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
response.json({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Config
|
||||||
|
router.get("/api/status-page/config", async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
let config = await getSettings("statusPage");
|
||||||
|
|
||||||
|
if (! config.statusPageTheme) {
|
||||||
|
config.statusPageTheme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! config.statusPagePublished) {
|
||||||
|
config.statusPagePublished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! config.title) {
|
||||||
|
config.title = "Uptime Kuma";
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page - Get the current Incident
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/incident", async (_, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||||
|
|
||||||
|
if (incident) {
|
||||||
|
incident = incident.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
ok: true,
|
||||||
|
incident,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page - Monitor List
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
const publicGroupList = [];
|
||||||
|
let list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
publicGroupList.push(await groupBean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(publicGroupList);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Polling Data
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
|
||||||
|
let heartbeatList = {};
|
||||||
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let monitorIDList = await R.getCol(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND public = 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (let monitorID of monitorIDList) {
|
||||||
|
let list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 50
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
list = R.convertToBeans("heartbeat", list);
|
||||||
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
|
||||||
|
const type = 24;
|
||||||
|
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
heartbeatList,
|
||||||
|
uptimeList
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkPublished() {
|
||||||
|
if (! await isPublished()) {
|
||||||
|
throw new Error("The status page is not published");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default is published
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function isPublished() {
|
||||||
|
const value = await setting("statusPagePublished");
|
||||||
|
if (value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function send403(res, msg = "") {
|
||||||
|
res.status(403).json({
|
||||||
|
"status": "fail",
|
||||||
|
"msg": msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
692
server/server.js
692
server/server.js
File diff suppressed because it is too large
Load diff
161
server/socket-handlers/status-page-socket-handler.js
Normal file
161
server/socket-handlers/status-page-socket-handler.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { checkLogin, setSettings } = require("../util-server");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { debug } = require("../../src/util");
|
||||||
|
const ImageDataURI = require("../image-data-uri");
|
||||||
|
const Database = require("../database");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
|
||||||
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
|
// Post or edit incident
|
||||||
|
socket.on("postIncident", async (incident, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 ");
|
||||||
|
|
||||||
|
let incidentBean;
|
||||||
|
|
||||||
|
if (incident.id) {
|
||||||
|
incidentBean = await R.findOne("incident", " id = ?", [
|
||||||
|
incident.id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incidentBean == null) {
|
||||||
|
incidentBean = R.dispense("incident");
|
||||||
|
}
|
||||||
|
|
||||||
|
incidentBean.title = incident.title;
|
||||||
|
incidentBean.content = incident.content;
|
||||||
|
incidentBean.style = incident.style;
|
||||||
|
incidentBean.pin = true;
|
||||||
|
|
||||||
|
if (incident.id) {
|
||||||
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
|
} else {
|
||||||
|
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.store(incidentBean);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
incident: incidentBean.toPublicJSON(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("unpinIncident", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save Status Page
|
||||||
|
// imgDataUrl Only Accept PNG!
|
||||||
|
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
|
// Check logo format
|
||||||
|
// If is image data url, convert to png file
|
||||||
|
// Else assume it is a url, nothing to do
|
||||||
|
if (imgDataUrl.startsWith("data:")) {
|
||||||
|
if (! imgDataUrl.startsWith(header)) {
|
||||||
|
throw new Error("Only allowed PNG logo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to file
|
||||||
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||||
|
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
config.icon = imgDataUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Config
|
||||||
|
await setSettings("statusPage", config);
|
||||||
|
|
||||||
|
// Save Public Group List
|
||||||
|
const groupIDList = [];
|
||||||
|
let groupOrder = 1;
|
||||||
|
|
||||||
|
for (let group of publicGroupList) {
|
||||||
|
let groupBean;
|
||||||
|
if (group.id) {
|
||||||
|
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||||
|
group.id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
groupBean = R.dispense("group");
|
||||||
|
}
|
||||||
|
|
||||||
|
groupBean.name = group.name;
|
||||||
|
groupBean.public = true;
|
||||||
|
groupBean.weight = groupOrder++;
|
||||||
|
|
||||||
|
await R.store(groupBean);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
|
||||||
|
groupBean.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
let monitorOrder = 1;
|
||||||
|
console.log(group.monitorList);
|
||||||
|
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
let relationBean = R.dispense("monitor_group");
|
||||||
|
relationBean.weight = monitorOrder++;
|
||||||
|
relationBean.group_id = groupBean.id;
|
||||||
|
relationBean.monitor_id = monitor.id;
|
||||||
|
await R.store(relationBean);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDList.push(groupBean.id);
|
||||||
|
group.id = groupBean.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete groups that not in the list
|
||||||
|
debug("Delete groups that not in the list");
|
||||||
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
publicGroupList,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
|
@ -5,6 +5,7 @@ const { debug } = require("../src/util");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init or reset JWT secret
|
* Init or reset JWT secret
|
||||||
|
@ -23,7 +24,7 @@ exports.initJWTSecret = async () => {
|
||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.tcping = function (hostname, port) {
|
exports.tcping = function (hostname, port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -44,7 +45,7 @@ exports.tcping = function (hostname, port) {
|
||||||
resolve(Math.round(data.max));
|
resolve(Math.round(data.max));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.ping = async (hostname) => {
|
exports.ping = async (hostname) => {
|
||||||
try {
|
try {
|
||||||
|
@ -57,7 +58,7 @@ exports.ping = async (hostname) => {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -69,13 +70,13 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (ms === null) {
|
} else if (ms === null) {
|
||||||
reject(new Error(stdout))
|
reject(new Error(stdout));
|
||||||
} else {
|
} else {
|
||||||
resolve(Math.round(ms))
|
resolve(Math.round(ms));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
|
@ -98,8 +99,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
|
@ -108,29 +109,29 @@ exports.setting = async function (key) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const v = JSON.parse(value);
|
const v = JSON.parse(value);
|
||||||
debug(`Get Setting: ${key}: ${v}`)
|
debug(`Get Setting: ${key}: ${v}`);
|
||||||
return v;
|
return v;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSetting = async function (key, value) {
|
exports.setSetting = async function (key, value) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
])
|
]);
|
||||||
if (!bean) {
|
if (!bean) {
|
||||||
bean = R.dispense("setting")
|
bean = R.dispense("setting");
|
||||||
bean.key = key;
|
bean.key = key;
|
||||||
}
|
}
|
||||||
bean.value = JSON.stringify(value);
|
bean.value = JSON.stringify(value);
|
||||||
await R.store(bean)
|
await R.store(bean);
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
type,
|
type,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
|
@ -143,7 +144,7 @@ exports.getSettings = async function (type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
let keyList = Object.keys(data);
|
||||||
|
@ -163,12 +164,12 @@ exports.setSettings = async function (type, data) {
|
||||||
|
|
||||||
if (bean.type === type) {
|
if (bean.type === type) {
|
||||||
bean.value = JSON.stringify(data[key]);
|
bean.value = JSON.stringify(data[key]);
|
||||||
promiseList.push(R.store(bean))
|
promiseList.push(R.store(bean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promiseList);
|
await Promise.all(promiseList);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
// param: res - response object from axios
|
// param: res - response object from axios
|
||||||
|
@ -185,40 +186,44 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||||
return daysRemaining;
|
return daysRemaining;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.checkCertificate = function (res) {
|
// Fix certificate Info for display
|
||||||
const {
|
// param: info - the chain obtained from getPeerCertificate()
|
||||||
valid_from,
|
const parseCertificateInfo = function (info) {
|
||||||
valid_to,
|
let link = info;
|
||||||
subjectaltname,
|
|
||||||
issuer,
|
|
||||||
fingerprint,
|
|
||||||
} = res.request.res.socket.getPeerCertificate(false);
|
|
||||||
|
|
||||||
if (!valid_from || !valid_to || !subjectaltname) {
|
while (link) {
|
||||||
throw {
|
if (!link.valid_from || !link.valid_to) {
|
||||||
message: "No TLS certificate in response",
|
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 valid = res.request.res.socket.authorized || false;
|
||||||
|
|
||||||
const validTo = new Date(valid_to);
|
const parsedInfo = parseCertificateInfo(info);
|
||||||
|
|
||||||
const validFor = subjectaltname
|
|
||||||
.replace(/DNS:|IP Address:/g, "")
|
|
||||||
.split(", ");
|
|
||||||
|
|
||||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid,
|
valid: valid,
|
||||||
validFor,
|
certInfo: parsedInfo
|
||||||
validTo,
|
|
||||||
daysRemaining,
|
|
||||||
issuer,
|
|
||||||
fingerprint,
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Check if the provided status code is within the accepted ranges
|
// Check if the provided status code is within the accepted ranges
|
||||||
// Param: status - the status code to check
|
// Param: status - the status code to check
|
||||||
|
@ -247,7 +252,7 @@ exports.checkStatusCode = function (status, accepted_codes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getTotalClientInRoom = (io, roomName) => {
|
exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
|
|
||||||
|
@ -270,14 +275,40 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.genSecret = () => {
|
exports.allowDevAllOrigin = (res) => {
|
||||||
let secret = "";
|
if (process.env.NODE_ENV === "development") {
|
||||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
exports.allowAllOrigin(res);
|
||||||
let charsLength = chars.length;
|
|
||||||
for ( let i = 0; i < 64; i++ ) {
|
|
||||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
|
||||||
}
|
}
|
||||||
return secret;
|
};
|
||||||
}
|
|
||||||
|
exports.allowAllOrigin = (res) => {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.checkLogin = (socket) => {
|
||||||
|
if (! socket.userID) {
|
||||||
|
throw new Error("You are not logged in.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {}
|
import { setPageLocale } from "./util-frontend";
|
||||||
|
export default {
|
||||||
|
created() {
|
||||||
|
setPageLocale();
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "vars.scss";
|
@import "vars.scss";
|
||||||
|
@import "multiselect.scss";
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
@ -144,7 +145,9 @@ h2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
background-color: $dark-bg;
|
&:not(.alert) {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
|
@ -177,6 +180,11 @@ h2 {
|
||||||
border-color: $dark-border-color;
|
border-color: $dark-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-control:disabled, .form-control[readonly] {
|
||||||
|
background-color: #232f3b;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.table-hover > tbody > tr:hover {
|
.table-hover > tbody > tr:hover {
|
||||||
--bs-table-accent-bg: #070a10;
|
--bs-table-accent-bg: #070a10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
|
@ -231,28 +239,16 @@ h2 {
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiselect
|
.monitor-list {
|
||||||
.multiselect__tags {
|
.item {
|
||||||
background-color: $dark-bg2;
|
&:hover {
|
||||||
border-color: $dark-border-color;
|
background-color: $dark-bg2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__input, .multiselect__single {
|
&.active {
|
||||||
background-color: $dark-bg2;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
|
@ -268,6 +264,16 @@ h2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
&.bg-info,
|
||||||
|
&.bg-warning,
|
||||||
|
&.bg-danger,
|
||||||
|
&.bg-light {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -288,3 +294,123 @@ h2 {
|
||||||
transform: translateY(50px);
|
transform: translateY(50px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-leave-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-from,
|
||||||
|
.slide-fade-right-leave-to {
|
||||||
|
transform: translateX(50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
&.scrollbar {
|
||||||
|
min-height: calc(100vh - 240px);
|
||||||
|
max-height: calc(100vh - 30px);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 14px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #cdf8f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #122f21;
|
||||||
|
background-color: $primary;
|
||||||
|
border-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #055160;
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border-color: #cff4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #842029;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f8d7da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4caf50;
|
||||||
|
border-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable=true] {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
background-color: rgba(239, 239, 239, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0 solid #eee;
|
||||||
|
background-color: rgba(245, 245, 245, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(239, 239, 239, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: rgba(239, 239, 239, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
&::after {
|
||||||
|
margin-left: 5px;
|
||||||
|
content: "🖊️";
|
||||||
|
font-size: 13px;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-image-crop-upload .vicp-wrap {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: 50rem;
|
||||||
|
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>
|
|
@ -25,6 +25,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
heartbeatList: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -34,12 +38,19 @@ export default {
|
||||||
beatMargin: 4,
|
beatMargin: 4,
|
||||||
move: false,
|
move: false,
|
||||||
maxBeat: -1,
|
maxBeat: -1,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If heartbeatList is null, get it from $root.heartbeatList
|
||||||
|
*/
|
||||||
beatList() {
|
beatList() {
|
||||||
return this.$root.heartbeatList[this.monitorId]
|
if (this.heartbeatList === null) {
|
||||||
|
return this.$root.heartbeatList[this.monitorId];
|
||||||
|
} else {
|
||||||
|
return this.heartbeatList;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
shortBeatList() {
|
shortBeatList() {
|
||||||
|
@ -58,12 +69,12 @@ export default {
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
// Add empty placeholder
|
// Add empty placeholder
|
||||||
for (let i = start; i < 0; i++) {
|
for (let i = start; i < 0; i++) {
|
||||||
placeholders.push(0)
|
placeholders.push(0);
|
||||||
}
|
}
|
||||||
start = 0;
|
start = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return placeholders.concat(this.beatList.slice(start))
|
return placeholders.concat(this.beatList.slice(start));
|
||||||
},
|
},
|
||||||
|
|
||||||
wrapStyle() {
|
wrapStyle() {
|
||||||
|
@ -73,7 +84,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
padding: `${topBottom}px ${leftRight}px`,
|
padding: `${topBottom}px ${leftRight}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
barStyle() {
|
barStyle() {
|
||||||
|
@ -83,12 +94,12 @@ export default {
|
||||||
return {
|
return {
|
||||||
transition: "all ease-in-out 0.25s",
|
transition: "all ease-in-out 0.25s",
|
||||||
transform: `translateX(${width}px)`,
|
transform: `translateX(${width}px)`,
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
transform: "translateX(0)",
|
transform: "translateX(0)",
|
||||||
}
|
};
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -98,7 +109,7 @@ export default {
|
||||||
height: this.beatHeight + "px",
|
height: this.beatHeight + "px",
|
||||||
margin: this.beatMargin + "px",
|
margin: this.beatMargin + "px",
|
||||||
"--hover-scale": this.hoverScale,
|
"--hover-scale": this.hoverScale,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -109,7 +120,7 @@ export default {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.move = false;
|
this.move = false;
|
||||||
}, 300)
|
}, 300);
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
@ -118,8 +129,10 @@ export default {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
if (this.heartbeatList === null) {
|
||||||
this.$root.heartbeatList[this.monitorId] = [];
|
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||||
|
this.$root.heartbeatList[this.monitorId] = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -149,7 +162,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -157,7 +170,7 @@ export default {
|
||||||
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -170,6 +183,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hp-bar-big {
|
.hp-bar-big {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
.beat {
|
.beat {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
||||||
token: "",
|
token: "",
|
||||||
res: null,
|
res: null,
|
||||||
tokenRequired: false,
|
tokenRequired: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
|
@ -60,21 +60,19 @@ export default {
|
||||||
|
|
||||||
this.$root.login(this.username, this.password, this.token, (res) => {
|
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
console.log(res)
|
|
||||||
|
|
||||||
if (res.tokenRequired) {
|
if (res.tokenRequired) {
|
||||||
this.tokenRequired = true;
|
this.tokenRequired = true;
|
||||||
} else {
|
} else {
|
||||||
this.res = res;
|
this.res = res;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -82,8 +80,17 @@ export default {
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form-floating {
|
||||||
|
> label {
|
||||||
|
padding-left: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .form-control {
|
||||||
|
padding-left: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
|
@ -1,44 +1,69 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
|
<div class="shadow-box mb-3">
|
||||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
<div class="list-header">
|
||||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
<div class="placeholder"></div>
|
||||||
|
<div class="search-wrapper">
|
||||||
|
<a v-if="!searchText" class="search-icon">
|
||||||
|
<font-awesome-icon icon="search" />
|
||||||
|
</a>
|
||||||
|
<a v-if="searchText" class="search-icon" @click="clearSearchText">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
</a>
|
||||||
|
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
|
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||||
|
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
<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="row">
|
||||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar === 'bottom' || $root.userHeartbeatBar === 'none' }">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<Uptime :monitor="item" type="24" :pill="true" />
|
<Uptime :monitor="item" type="24" :pill="true" />
|
||||||
{{ item.name }}
|
<span class="ms-1">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="$root.userHeartbeatBar === 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4 small-padding">
|
||||||
|
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
|
||||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
<div v-if="$root.userHeartbeatBar === 'bottom'" class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</router-link>
|
||||||
</router-link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||||
import Uptime from "../components/Uptime.vue";
|
import Uptime from "../components/Uptime.vue";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Uptime,
|
Uptime,
|
||||||
HeartbeatBar,
|
HeartbeatBar,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchText: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
@ -66,7 +91,18 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return m1.name.localeCompare(m2.name);
|
return m1.name.localeCompare(m2.name);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Simple filter by search text
|
||||||
|
// finds monitor name, tag name or tag value
|
||||||
|
if (this.searchText) {
|
||||||
|
const loweredSearchText = this.searchText.toLowerCase();
|
||||||
|
result = result.filter(monitor => {
|
||||||
|
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||||
|
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||||
|
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -75,8 +111,11 @@ export default {
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return "/dashboard/" + id;
|
return "/dashboard/" + id;
|
||||||
},
|
},
|
||||||
|
clearSearchText() {
|
||||||
|
this.searchText = "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -87,57 +126,51 @@ export default {
|
||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list-header {
|
||||||
&.scrollbar {
|
border-bottom: 1px solid #dee2e6;
|
||||||
min-height: calc(100vh - 240px);
|
border-radius: 10px 10px 0 0;
|
||||||
max-height: calc(100vh - 30px);
|
margin: -10px;
|
||||||
overflow-y: auto;
|
margin-bottom: 10px;
|
||||||
position: sticky;
|
padding: 10px;
|
||||||
top: 10px;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
|
||||||
.item {
|
.dark & {
|
||||||
display: block;
|
background-color: #161b22;
|
||||||
text-decoration: none;
|
border-bottom: 0;
|
||||||
padding: 13px 15px 10px 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all ease-in-out 0.15s;
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $highlight-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: #cdf8f4;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
@media (max-width: 770px) {
|
||||||
.list {
|
.list-header {
|
||||||
.item {
|
margin: -20px;
|
||||||
&:hover {
|
margin-bottom: 10px;
|
||||||
background-color: $dark-bg2;
|
padding: 5px;
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $dark-bg2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
padding: 10px;
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
.monitorItem {
|
.monitorItem {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
padding-left: 62px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,22 +13,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||||
<select id="notification-type" v-model="notification.type" class="form-select">
|
<select id="notification-type" v-model="notification.type" class="form-select">
|
||||||
<option value="telegram">Telegram</option>
|
<option v-for="type in notificationTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||||
<option value="webhook">Webhook</option>
|
|
||||||
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
|
|
||||||
<option value="discord">Discord</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>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -37,373 +22,8 @@
|
||||||
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
|
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Telegram v-if="notification.type === 'telegram'" />
|
<!-- form body -->
|
||||||
|
<component :is="currentForm" />
|
||||||
<!-- 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>
|
|
||||||
</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-version" class="form-label">Octopush API Version</label>
|
|
||||||
<select id="octopush-version" v-model="notification.octopushVersion" class="form-select">
|
|
||||||
<option value="2">V2</option>
|
|
||||||
<option value="1">V1 (Legacy Octopush-DM)</option>
|
|
||||||
</select>
|
|
||||||
<div class="form-text">
|
|
||||||
Do you use the legacy version of Octopush (2011-2020) or the new version ?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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" -->
|
|
||||||
|
|
||||||
<div class="mb-3 mt-4">
|
<div class="mb-3 mt-4">
|
||||||
<hr class="dropdown-divider mb-4">
|
<hr class="dropdown-divider mb-4">
|
||||||
|
@ -452,16 +72,11 @@ import { Modal } from "bootstrap"
|
||||||
import { ucfirst } from "../util.ts"
|
import { ucfirst } from "../util.ts"
|
||||||
|
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
import HiddenInput from "./HiddenInput.vue";
|
import NotificationFormList from "./notifications"
|
||||||
import Telegram from "./notifications/Telegram.vue";
|
|
||||||
import SMTP from "./notifications/SMTP.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Confirm,
|
Confirm,
|
||||||
HiddenInput,
|
|
||||||
Telegram,
|
|
||||||
SMTP,
|
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
emits: ["added"],
|
emits: ["added"],
|
||||||
|
@ -470,13 +85,23 @@ export default {
|
||||||
model: null,
|
model: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
id: null,
|
id: null,
|
||||||
|
notificationTypes: Object.keys(NotificationFormList),
|
||||||
notification: {
|
notification: {
|
||||||
name: "",
|
name: "",
|
||||||
|
/** @type { null | keyof NotificationFormList } */
|
||||||
type: null,
|
type: null,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
// Do not set default value here, please scroll to show()
|
// 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]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -497,10 +122,6 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal)
|
||||||
|
|
||||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
|
||||||
this.appriseInstalled = installed;
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
@ -528,9 +149,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Default value here
|
// Set Default value here
|
||||||
this.notification.type = "telegram";
|
this.notification.type = this.notificationTypes[0];
|
||||||
this.notification.gotifyPriority = 8;
|
|
||||||
this.notification.smtpSecure = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modal.show()
|
this.modal.show()
|
||||||
|
|
144
src/components/PublicGroupList.vue
Normal file
144
src/components/PublicGroupList.vue
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<!-- Group List -->
|
||||||
|
<Draggable
|
||||||
|
v-model="$root.publicGroupList"
|
||||||
|
:disabled="!editMode"
|
||||||
|
item-key="id"
|
||||||
|
:animation="100"
|
||||||
|
>
|
||||||
|
<template #item="group">
|
||||||
|
<div class="mb-5 ">
|
||||||
|
<!-- Group Title -->
|
||||||
|
<h2 class="group-title">
|
||||||
|
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||||
|
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||||
|
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
|
||||||
|
{{ $t("No Monitors") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monitor List -->
|
||||||
|
<!-- animation is not working, no idea why -->
|
||||||
|
<Draggable
|
||||||
|
v-model="group.element.monitorList"
|
||||||
|
class="monitor-list"
|
||||||
|
group="same-group"
|
||||||
|
:disabled="!editMode"
|
||||||
|
:animation="100"
|
||||||
|
item-key="id"
|
||||||
|
>
|
||||||
|
<template #item="monitor">
|
||||||
|
<div class="item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9 col-md-8 small-padding">
|
||||||
|
<div class="info">
|
||||||
|
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
|
|
||||||
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
|
{{ monitor.element.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
|
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Draggable from "vuedraggable";
|
||||||
|
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||||
|
import Uptime from "./Uptime.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Draggable,
|
||||||
|
HeartbeatBar,
|
||||||
|
Uptime,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showGroupDrag() {
|
||||||
|
return (this.$root.publicGroupList.length >= 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeGroup(index) {
|
||||||
|
this.$root.publicGroupList.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMonitor(groupIndex, index) {
|
||||||
|
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars";
|
||||||
|
|
||||||
|
.no-monitor-msg {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
min-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag {
|
||||||
|
color: #bbb;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.item {
|
||||||
|
padding: 13px 0 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
73
src/components/Tag.vue
Normal file
73
src/components/Tag.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="tag-wrapper rounded d-inline-flex"
|
||||||
|
:class="{ 'px-3': size == 'normal',
|
||||||
|
'py-1': size == 'normal',
|
||||||
|
'm-2': size == 'normal',
|
||||||
|
'px-2': size == 'sm',
|
||||||
|
'py-0': size == 'sm',
|
||||||
|
'm-1': size == 'sm',
|
||||||
|
}"
|
||||||
|
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||||
|
>
|
||||||
|
<span class="tag-text">{{ displayText }}</span>
|
||||||
|
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "normal",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayText() {
|
||||||
|
if (this.item.value == "") {
|
||||||
|
return this.item.name;
|
||||||
|
} else {
|
||||||
|
return `${this.item.name}: ${this.item.value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag-wrapper {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-text {
|
||||||
|
padding-bottom: 1px !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 24px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
405
src/components/TagsManager.vue
Normal file
405
src/components/TagsManager.vue
Normal file
|
@ -0,0 +1,405 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||||
|
<div class="mb-3 p-1">
|
||||||
|
<tag
|
||||||
|
v-for="item in selectedTags"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
:remove="deleteTag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-add"
|
||||||
|
:disabled="processing"
|
||||||
|
@click.stop="showAddDialog"
|
||||||
|
>
|
||||||
|
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.select"
|
||||||
|
class="mb-2"
|
||||||
|
:options="tagOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('Add New below or Select...')"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
||||||
|
<div class="w-50 pe-2">
|
||||||
|
<input v-model="newDraftTag.name" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||||
|
:placeholder="$t('Name')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this name already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-50 ps-2">
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.color"
|
||||||
|
:options="colorOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('color')"
|
||||||
|
track-by="color"
|
||||||
|
label="name"
|
||||||
|
select-label=""
|
||||||
|
deselect-label=""
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<input v-model="newDraftTag.value" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||||
|
:placeholder="$t('value (optional)')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this value already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary float-end"
|
||||||
|
:disabled="processing || validateDraftTag.invalid"
|
||||||
|
@click.stop="addDraftTag"
|
||||||
|
>
|
||||||
|
{{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
import { useToast } from "vue-toastification"
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Tag,
|
||||||
|
VueMultiselect,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
preSelectedTags: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: null,
|
||||||
|
existingTags: [],
|
||||||
|
processing: false,
|
||||||
|
newTags: [],
|
||||||
|
deleteTags: [],
|
||||||
|
newDraftTag: {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tagOptions() {
|
||||||
|
const tagOptions = this.existingTags;
|
||||||
|
for (const tag of this.newTags) {
|
||||||
|
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
||||||
|
tagOptions.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tagOptions;
|
||||||
|
},
|
||||||
|
selectedTags() {
|
||||||
|
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
||||||
|
},
|
||||||
|
colorOptions() {
|
||||||
|
return [
|
||||||
|
{ name: this.$t("Gray"),
|
||||||
|
color: "#4B5563" },
|
||||||
|
{ name: this.$t("Red"),
|
||||||
|
color: "#DC2626" },
|
||||||
|
{ name: this.$t("Orange"),
|
||||||
|
color: "#D97706" },
|
||||||
|
{ name: this.$t("Green"),
|
||||||
|
color: "#059669" },
|
||||||
|
{ name: this.$t("Blue"),
|
||||||
|
color: "#2563EB" },
|
||||||
|
{ name: this.$t("Indigo"),
|
||||||
|
color: "#4F46E5" },
|
||||||
|
{ name: this.$t("Purple"),
|
||||||
|
color: "#7C3AED" },
|
||||||
|
{ name: this.$t("Pink"),
|
||||||
|
color: "#DB2777" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
validateDraftTag() {
|
||||||
|
let nameInvalid = false;
|
||||||
|
let valueInvalid = false;
|
||||||
|
let invalid = true;
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a Tag
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
invalid = false;
|
||||||
|
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||||
|
// Try to create new tag with existing name
|
||||||
|
nameInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||||
|
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
||||||
|
) || (
|
||||||
|
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
||||||
|
)).length > 0) {
|
||||||
|
// Try to add a tag with existing name and value
|
||||||
|
valueInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newDraftTag.select != null) {
|
||||||
|
// Select an existing tag, no need to validate
|
||||||
|
invalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
|
||||||
|
// Missing form inputs
|
||||||
|
nameInvalid = false;
|
||||||
|
invalid = true;
|
||||||
|
} else {
|
||||||
|
// Looks valid
|
||||||
|
invalid = false;
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
invalid,
|
||||||
|
nameInvalid,
|
||||||
|
valueInvalid,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
this.getExistingTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAddDialog() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
getExistingTags() {
|
||||||
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.existingTags = res.tags;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteTag(item) {
|
||||||
|
if (item.new) {
|
||||||
|
// Undo Adding a new Tag
|
||||||
|
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
|
||||||
|
} else {
|
||||||
|
// Remove an Existing Tag
|
||||||
|
this.deleteTags.push(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textColor(option) {
|
||||||
|
if (option.color) {
|
||||||
|
return "white";
|
||||||
|
} else {
|
||||||
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addDraftTag() {
|
||||||
|
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||||
|
if (this.newDraftTag.select != null) {
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a tag
|
||||||
|
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
||||||
|
} else {
|
||||||
|
// Add an existing Tag
|
||||||
|
this.newTags.push({
|
||||||
|
id: this.newDraftTag.select.id,
|
||||||
|
color: this.newDraftTag.select.color,
|
||||||
|
name: this.newDraftTag.select.name,
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new Tag
|
||||||
|
this.newTags.push({
|
||||||
|
color: this.newDraftTag.color.color,
|
||||||
|
name: this.newDraftTag.name.trim(),
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.clearDraftTag();
|
||||||
|
},
|
||||||
|
clearDraftTag() {
|
||||||
|
this.newDraftTag = {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
};
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
addTagAsync(newTag) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addMonitorTagAsync(tagId, monitorId, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEnter() {
|
||||||
|
if (!this.validateDraftTag.invalid) {
|
||||||
|
this.addDraftTag();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit(monitorId) {
|
||||||
|
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
for (const newTag of this.newTags) {
|
||||||
|
let tagId;
|
||||||
|
if (newTag.id == null) {
|
||||||
|
// Create a New Tag
|
||||||
|
let newTagResult;
|
||||||
|
await this.addTagAsync(newTag).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newTagResult = false;
|
||||||
|
}
|
||||||
|
newTagResult = res.tag;
|
||||||
|
});
|
||||||
|
if (!newTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tagId = newTagResult.id;
|
||||||
|
// Assign the new ID to the tags of the same name & color
|
||||||
|
this.newTags.map(tag => {
|
||||||
|
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||||
|
tag.id = newTagResult.id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tagId = newTag.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMonitorTagResult;
|
||||||
|
// Assign tag to monitor
|
||||||
|
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
newMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!newMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deleteTag of this.deleteTags) {
|
||||||
|
let deleteMonitorTagResult;
|
||||||
|
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
deleteMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
deleteMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!deleteMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getExistingTags();
|
||||||
|
this.newTags = [];
|
||||||
|
this.deleteTags = [];
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn-add {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -22,33 +22,33 @@ export default {
|
||||||
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$t("notAvailableShort")
|
return this.$t("notAvailableShort");
|
||||||
},
|
},
|
||||||
|
|
||||||
color() {
|
color() {
|
||||||
if (this.lastHeartBeat.status === 0) {
|
if (this.lastHeartBeat.status === 0) {
|
||||||
return "danger"
|
return "danger";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastHeartBeat.status === 1) {
|
if (this.lastHeartBeat.status === 1) {
|
||||||
return "primary"
|
return "primary";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastHeartBeat.status === 2) {
|
if (this.lastHeartBeat.status === 2) {
|
||||||
return "warning"
|
return "warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "secondary"
|
return "secondary";
|
||||||
},
|
},
|
||||||
|
|
||||||
lastHeartBeat() {
|
lastHeartBeat() {
|
||||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
return this.$root.lastHeartbeatList[this.monitor.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: -1,
|
status: -1,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
|
@ -59,7 +59,7 @@ export default {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
34
src/components/notifications/Apprise.vue
Normal file
34
src/components/notifications/Apprise.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||||
|
<input id="apprise-url" v-model="$parent.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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
appriseInstalled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||||
|
this.appriseInstalled = installed;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
20
src/components/notifications/Discord.vue
Normal file
20
src/components/notifications/Discord.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="discord-webhook-url" class="form-label">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">
|
||||||
|
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="$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">Prefix Custom Message</label>
|
||||||
|
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
|
||||||
|
</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">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">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">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">Channel access token</label>
|
||||||
|
<HiddenInput id="line-channel-access-token" v-model="$parent.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="$parent.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>
|
||||||
|
|
||||||
|
<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">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>Required</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
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">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">Username</label>
|
||||||
|
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
|
||||||
|
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||||
|
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
|
||||||
|
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
|
||||||
|
<label for="mattermost-channel" class="form-label">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>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>
|
40
src/components/notifications/Octopush.vue
Normal file
40
src/components/notifications/Octopush.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<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">SMS Type</label>
|
||||||
|
<select id="octopush-type-sms" v-model="$parent.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="$parent.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="$parent.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>
|
||||||
|
|
||||||
|
<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">Access Token</label>
|
||||||
|
<HiddenInput id="pushbullet-access-token" v-model="$parent.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>
|
||||||
|
|
||||||
|
<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">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">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">Device</label>
|
||||||
|
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
||||||
|
<label for="pushover-device" class="form-label">Message Title</label>
|
||||||
|
<input id="pushover-title" v-model="$parent.notification.pushovertitle" type="text" class="form-control">
|
||||||
|
<label for="pushover-priority" class="form-label">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">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>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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
29
src/components/notifications/RocketChat.vue
Normal file
29
src/components/notifications/RocketChat.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<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="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||||
|
<label for="rocket-username" class="form-label">Username</label>
|
||||||
|
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
|
||||||
|
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
|
||||||
|
<label for="rocket-channel" class="form-label">Channel Name</label>
|
||||||
|
<input id="rocket-channel-name" v-model="$parent.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="$parent.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>
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||||
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -45,17 +45,17 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-email" class="form-label">To Email</label>
|
<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">
|
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-cc" class="form-label">CC</label>
|
<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">
|
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-bcc" class="form-label">BCC</label>
|
<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">
|
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -66,10 +66,19 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
data() {
|
computed: {
|
||||||
return {
|
hasRecipient() {
|
||||||
name: "smtp",
|
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>
|
</script>
|
||||||
|
|
32
src/components/notifications/Signal.vue
Normal file
32
src/components/notifications/Signal.vue
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="signal-url" class="form-label">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">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">Recipients</label>
|
||||||
|
<input id="signal-recipients" v-model="$parent.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>
|
29
src/components/notifications/Slack.vue
Normal file
29
src/components/notifications/Slack.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<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="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
||||||
|
<label for="slack-username" class="form-label">Username</label>
|
||||||
|
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
||||||
|
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
||||||
|
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||||
|
<input id="slack-channel-name" v-model="$parent.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="$parent.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>
|
19
src/components/notifications/Teams.vue
Normal file
19
src/components/notifications/Teams.vue
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
|
||||||
|
<input
|
||||||
|
id="teams-webhookurl"
|
||||||
|
v-model="$parent.notification.webhookUrl"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
You can learn how to create a webhook url
|
||||||
|
<a
|
||||||
|
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
|
||||||
|
target="_blank"
|
||||||
|
>here</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -47,11 +47,6 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: "telegram",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
telegramGetUpdatesURL() {
|
telegramGetUpdatesURL() {
|
||||||
let token = "<YOUR BOT TOKEN HERE>"
|
let token = "<YOUR BOT TOKEN HERE>"
|
||||||
|
@ -62,9 +57,6 @@ export default {
|
||||||
|
|
||||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||||
},
|
},
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async autoGetTelegramChatID() {
|
async autoGetTelegramChatID() {
|
||||||
|
|
23
src/components/notifications/Webhook.vue
Normal file
23
src/components/notifications/Webhook.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-url" class="form-label">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">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>"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>
|
44
src/components/notifications/index.js
Normal file
44
src/components/notifications/index.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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 LunaSea from "./LunaSea.vue";
|
||||||
|
import Apprise from "./Apprise.vue";
|
||||||
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
|
import Line from "./Line.vue";
|
||||||
|
import Mattermost from "./Mattermost.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,
|
||||||
|
"lunasea": LunaSea,
|
||||||
|
"apprise": Apprise,
|
||||||
|
"pushbullet": Pushbullet,
|
||||||
|
"line": Line,
|
||||||
|
"mattermost": Mattermost
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationFormList
|
63
src/i18n.js
Normal file
63
src/i18n.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import bgBG from "./languages/bg-BG";
|
||||||
|
import daDK from "./languages/da-DK";
|
||||||
|
import deDE from "./languages/de-DE";
|
||||||
|
import en from "./languages/en";
|
||||||
|
import fa from "./languages/fa";
|
||||||
|
import esEs from "./languages/es-ES";
|
||||||
|
import ptBR from "./languages/pt-BR";
|
||||||
|
import etEE from "./languages/et-EE";
|
||||||
|
import frFR from "./languages/fr-FR";
|
||||||
|
import hu from "./languages/hu";
|
||||||
|
import itIT from "./languages/it-IT";
|
||||||
|
import ja from "./languages/ja";
|
||||||
|
import koKR from "./languages/ko-KR";
|
||||||
|
import nlNL from "./languages/nl-NL";
|
||||||
|
import pl from "./languages/pl";
|
||||||
|
import ruRU from "./languages/ru-RU";
|
||||||
|
import sr from "./languages/sr";
|
||||||
|
import srLatn from "./languages/sr-latn";
|
||||||
|
import trTR from "./languages/tr-TR";
|
||||||
|
import svSE from "./languages/sv-SE";
|
||||||
|
import zhCN from "./languages/zh-CN";
|
||||||
|
import zhHK from "./languages/zh-HK";
|
||||||
|
|
||||||
|
const languageList = {
|
||||||
|
en,
|
||||||
|
"zh-HK": zhHK,
|
||||||
|
"bg-BG": bgBG,
|
||||||
|
"de-DE": deDE,
|
||||||
|
"nl-NL": nlNL,
|
||||||
|
"es-ES": esEs,
|
||||||
|
"fa": fa,
|
||||||
|
"pt-BR": ptBR,
|
||||||
|
"fr-FR": frFR,
|
||||||
|
"hu": hu,
|
||||||
|
"it-IT": itIT,
|
||||||
|
"ja": ja,
|
||||||
|
"da-DK": daDK,
|
||||||
|
"sr": sr,
|
||||||
|
"sr-latn": srLatn,
|
||||||
|
"sv-SE": svSE,
|
||||||
|
"tr-TR": trTR,
|
||||||
|
"ko-KR": koKR,
|
||||||
|
"ru-RU": ruRU,
|
||||||
|
"zh-CN": zhCN,
|
||||||
|
"pl": pl,
|
||||||
|
"et-EE": etEE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rtlLangs = ["fa"];
|
||||||
|
|
||||||
|
export const currentLocale = () => localStorage.locale || "en";
|
||||||
|
|
||||||
|
export const localeDirection = () => {
|
||||||
|
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr"
|
||||||
|
}
|
||||||
|
export const i18n = createI18n({
|
||||||
|
locale: currentLocale(),
|
||||||
|
fallbackLocale: "en",
|
||||||
|
silentFallbackWarn: true,
|
||||||
|
silentTranslationWarn: true,
|
||||||
|
messages: languageList,
|
||||||
|
});
|
75
src/icon.js
75
src/icon.js
|
@ -1,10 +1,71 @@
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
|
||||||
|
|
||||||
// Add Free Font Awesome Icons here
|
// Add Free Font Awesome Icons
|
||||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash);
|
import {
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTimesCircle,
|
||||||
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages,
|
||||||
|
faUpload,
|
||||||
|
faCopy,
|
||||||
|
faCheck,
|
||||||
|
faFile,
|
||||||
|
faAward,
|
||||||
|
faLink,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTimesCircle,
|
||||||
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages,
|
||||||
|
faUpload,
|
||||||
|
faCopy,
|
||||||
|
faCheck,
|
||||||
|
faFile,
|
||||||
|
faAward,
|
||||||
|
faLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
export { FontAwesomeIcon };
|
||||||
|
|
||||||
export { FontAwesomeIcon }
|
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
# How to translate
|
# How to translate
|
||||||
|
|
||||||
1. Fork this repo.
|
1. Fork this repo.
|
||||||
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
|
2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
|
||||||
3. `npm run update-language-files --base-lang=de-DE`
|
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
|
||||||
6. Your language file should be filled in. You can translate now.
|
4. Your language file should be filled in. You can translate now.
|
||||||
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
|
||||||
8. Import your language file in `src/main.js` and add it to `languageList` constant.
|
6. Import your language file in `src/i18n.js` and add it to `languageList` constant.
|
||||||
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
One of good examples:
|
One of good examples:
|
||||||
https://github.com/louislam/uptime-kuma/pull/316/files
|
https://github.com/louislam/uptime-kuma/pull/316/files
|
||||||
|
|
||||||
|
|
||||||
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏
|
||||||
|
|
||||||
|
|
181
src/languages/bg-BG.js
Normal file
181
src/languages/bg-BG.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
export default {
|
||||||
|
languageName: "Български",
|
||||||
|
checkEverySecond: "Проверявай на всеки {0} секунди.",
|
||||||
|
retryCheckEverySecond: "Повторен опит на всеки {0} секунди.",
|
||||||
|
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие",
|
||||||
|
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
|
||||||
|
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.",
|
||||||
|
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
|
||||||
|
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
|
||||||
|
passwordNotMatchMsg: "Повторената парола не съвпада.",
|
||||||
|
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.",
|
||||||
|
keywordDescription: "Търсете ключова дума в обикновен html или JSON отговор - чувствителна е към регистъра",
|
||||||
|
pauseDashboardHome: "Пауза",
|
||||||
|
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
||||||
|
deleteNotificationMsg: "Наистина ли желаете да изтриете известието за всички монитори?",
|
||||||
|
resoverserverDescription: "Cloudflare е сървърът по подразбиране, можете да промените сървъра по всяко време.",
|
||||||
|
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
||||||
|
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
||||||
|
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да изключите известието за всеки отделен монитор.",
|
||||||
|
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
||||||
|
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
||||||
|
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
||||||
|
importHandleDescription: "Изберете 'Пропусни съществуващите', ако искате да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
|
||||||
|
confirmImportMsg: "Сигурни ли сте за импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
||||||
|
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
||||||
|
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?",
|
||||||
|
Settings: "Настройки",
|
||||||
|
Dashboard: "Табло",
|
||||||
|
"New Update": "Нова актуализация",
|
||||||
|
Language: "Език",
|
||||||
|
Appearance: "Изглед",
|
||||||
|
Theme: "Тема",
|
||||||
|
General: "Общи",
|
||||||
|
Version: "Версия",
|
||||||
|
"Check Update On GitHub": "Провери за актуализация в GitHub",
|
||||||
|
List: "Списък",
|
||||||
|
Add: "Добави",
|
||||||
|
"Add New Monitor": "Добави монитор",
|
||||||
|
"Quick Stats": "Кратка статистика",
|
||||||
|
Up: "Достъпни",
|
||||||
|
Down: "Недостъпни",
|
||||||
|
Pending: "В изчакване",
|
||||||
|
Unknown: "Неизвестни",
|
||||||
|
Pause: "В пауза",
|
||||||
|
Name: "Име",
|
||||||
|
Status: "Статус",
|
||||||
|
DateTime: "Дата и час",
|
||||||
|
Message: "Съобщение",
|
||||||
|
"No important events": "Няма важни събития",
|
||||||
|
Resume: "Възобнови",
|
||||||
|
Edit: "Редактирай",
|
||||||
|
Delete: "Изтрий",
|
||||||
|
Current: "Текущ",
|
||||||
|
Uptime: "Време на работа",
|
||||||
|
"Cert Exp.": "Вал. сертификат",
|
||||||
|
days: "дни",
|
||||||
|
day: "ден",
|
||||||
|
"-day": "-ден",
|
||||||
|
hour: "час",
|
||||||
|
"-hour": "-час",
|
||||||
|
Response: "Отговор",
|
||||||
|
Ping: "Пинг",
|
||||||
|
"Monitor Type": "Монитор тип",
|
||||||
|
Keyword: "Ключова дума",
|
||||||
|
"Friendly Name": "Псевдоним",
|
||||||
|
URL: "URL Адрес",
|
||||||
|
Hostname: "Име на хост",
|
||||||
|
Port: "Порт",
|
||||||
|
"Heartbeat Interval": "Честота на проверка",
|
||||||
|
Retries: "Повторни опити",
|
||||||
|
"Heartbeat Retry Interval": "Честота на повторните опити",
|
||||||
|
Advanced: "Разширени",
|
||||||
|
"Upside Down Mode": "Обърнат режим",
|
||||||
|
"Max. Redirects": "Макс. брой пренасочвания",
|
||||||
|
"Accepted Status Codes": "Допустими статус кодове",
|
||||||
|
Save: "Запази",
|
||||||
|
Notifications: "Известявания",
|
||||||
|
"Not available, please setup.": "Не е налично. Моля, настройте.",
|
||||||
|
"Setup Notification": "Настройка за известяване",
|
||||||
|
Light: "Светла",
|
||||||
|
Dark: "Тъмна",
|
||||||
|
Auto: "Автоматично",
|
||||||
|
"Theme - Heartbeat Bar": "Тема - поле проверки",
|
||||||
|
Normal: "Нормално",
|
||||||
|
Bottom: "Долу",
|
||||||
|
None: "Без",
|
||||||
|
Timezone: "Часова зона",
|
||||||
|
"Search Engine Visibility": "Видимост за търсачки",
|
||||||
|
"Allow indexing": "Разреши индексиране",
|
||||||
|
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките",
|
||||||
|
"Change Password": "Промени парола",
|
||||||
|
"Current Password": "Текуща парола",
|
||||||
|
"New Password": "Нова парола",
|
||||||
|
"Repeat New Password": "Повторете новата парола",
|
||||||
|
"Update Password": "Актуализирай парола",
|
||||||
|
"Disable Auth": "Изключи удостоверяване",
|
||||||
|
"Enable Auth": "Включи удостоверяване",
|
||||||
|
Logout: "Изход от профила",
|
||||||
|
Leave: "Напускам",
|
||||||
|
"I understand, please disable": "Разбирам. Моля, изключи",
|
||||||
|
Confirm: "Потвърди",
|
||||||
|
Yes: "Да",
|
||||||
|
No: "Не",
|
||||||
|
Username: "Потребител",
|
||||||
|
Password: "Парола",
|
||||||
|
"Remember me": "Запомни ме",
|
||||||
|
Login: "Вход",
|
||||||
|
"No Monitors, please": "Моля, без монитори",
|
||||||
|
"add one": "добави един",
|
||||||
|
"Notification Type": "Тип известяване",
|
||||||
|
Email: "Имейл",
|
||||||
|
Test: "Тест",
|
||||||
|
"Certificate Info": "Информация за сертификат",
|
||||||
|
"Resolver Server": "Преобразуващ (DNS) сървър",
|
||||||
|
"Resource Record Type": "Тип запис",
|
||||||
|
"Last Result": "Последен резултат",
|
||||||
|
"Create your admin account": "Създаване на администриращ акаунт",
|
||||||
|
"Repeat Password": "Повторете паролата",
|
||||||
|
"Import Backup": "Импорт на архив",
|
||||||
|
"Export Backup": "Експорт на архив",
|
||||||
|
Export: "Експорт",
|
||||||
|
Import: "Импорт",
|
||||||
|
respTime: "Време за отговор (ms)",
|
||||||
|
notAvailableShort: "Няма",
|
||||||
|
"Default enabled": "Включен по подразбиране",
|
||||||
|
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
|
||||||
|
Create: "Създай",
|
||||||
|
"Clear Data": "Изчисти данни",
|
||||||
|
Events: "Събития",
|
||||||
|
Heartbeats: "Проверки",
|
||||||
|
"Auto Get": "Автоматияно получаване",
|
||||||
|
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
|
||||||
|
backupDescription2: "PS: Данни за история и събития не са включени.",
|
||||||
|
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||||
|
alertNoFile: "Моля, изберете файл за импортиране.",
|
||||||
|
alertWrongFileType: "Моля, изберете JSON файл.",
|
||||||
|
"Clear all statistics": "Изчисти всички статистики",
|
||||||
|
"Skip existing": "Пропусни съществуващите",
|
||||||
|
Overwrite: "Презапиши",
|
||||||
|
Options: "Опции",
|
||||||
|
"Keep both": "Запази двете",
|
||||||
|
"Verify Token": "Проверка на токен код",
|
||||||
|
"Setup 2FA": "Настройка 2FA",
|
||||||
|
"Enable 2FA": "Включи 2FA",
|
||||||
|
"Disable 2FA": "Изключи 2FA",
|
||||||
|
"2FA Settings": "Настройки 2FA",
|
||||||
|
"Two Factor Authentication": "Двуфакторно удостоверяване",
|
||||||
|
Active: "Активно",
|
||||||
|
Inactive: "Неактивно",
|
||||||
|
Token: "Токен код",
|
||||||
|
"Show URI": "Покажи URI",
|
||||||
|
Tags: "Етикети",
|
||||||
|
"Add New below or Select...": "Добави нов по-долу или избери...",
|
||||||
|
"Tag with this name already exist.": "Етикет с това име вече съществува.",
|
||||||
|
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.",
|
||||||
|
color: "цвят",
|
||||||
|
"value (optional)": "стойност (по желание)",
|
||||||
|
Gray: "Сиво",
|
||||||
|
Red: "Червено",
|
||||||
|
Orange: "Оранжево",
|
||||||
|
Green: "Зелено",
|
||||||
|
Blue: "Синьо",
|
||||||
|
Indigo: "Индиго",
|
||||||
|
Purple: "Лилаво",
|
||||||
|
Pink: "Розово",
|
||||||
|
"Search...": "Търси...",
|
||||||
|
"Avg. Ping": "Ср. пинг",
|
||||||
|
"Avg. Response": "Ср. отговор",
|
||||||
|
"Entry Page": "Основна страница",
|
||||||
|
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.",
|
||||||
|
"No Services": "Няма Услуги",
|
||||||
|
"All Systems Operational": "Всички системи функционират",
|
||||||
|
"Partially Degraded Service": "Частично влошена услуга",
|
||||||
|
"Degraded Service": "Влошена услуга",
|
||||||
|
"Add Group": "Добави група",
|
||||||
|
"Add a monitor": "Добави монитор",
|
||||||
|
"Edit Status Page": "Редактирай статус страница",
|
||||||
|
"Go to Dashboard": "Към Таблото",
|
||||||
|
};
|
|
@ -36,7 +36,6 @@ export default {
|
||||||
hour: "Timer",
|
hour: "Timer",
|
||||||
"-hour": "-Timer",
|
"-hour": "-Timer",
|
||||||
checkEverySecond: "Tjek hvert {0} sekund",
|
checkEverySecond: "Tjek hvert {0} sekund",
|
||||||
"Avg.": "Gns.",
|
|
||||||
Response: "Respons",
|
Response: "Respons",
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Overvåger Type",
|
"Monitor Type": "Overvåger Type",
|
||||||
|
@ -120,7 +119,6 @@ export default {
|
||||||
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
|
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
|
||||||
"Default enabled": "Standard aktiveret",
|
"Default enabled": "Standard aktiveret",
|
||||||
"Also apply to existing monitors": "Anvend også på eksisterende overvågere",
|
"Also apply to existing monitors": "Anvend også på eksisterende overvågere",
|
||||||
"Import/Export Backup": " Importér/Eksportér sikkerhedskopi",
|
|
||||||
Export: "Eksport",
|
Export: "Eksport",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
|
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
|
||||||
|
@ -128,20 +126,57 @@ export default {
|
||||||
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
|
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
|
||||||
alertNoFile: "Vælg en fil der skal importeres.",
|
alertNoFile: "Vælg en fil der skal importeres.",
|
||||||
alertWrongFileType: "Vælg venligst en JSON-fil.",
|
alertWrongFileType: "Vælg venligst en JSON-fil.",
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
twoFAVerifyLabel: "Indtast venligst dit token for at bekræfte, at 2FA fungerer",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
tokenValidSettingsMsg: "Token er gyldigt! Du kan nu gemme 2FA -indstillingerne.",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
confirmEnableTwoFAMsg: "Er du sikker på at du vil aktivere 2FA?",
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
confirmDisableTwoFAMsg: "Er du sikker på at du vil deaktivere 2FA?",
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
"Apply on all existing monitors": "Anvend på alle eksisterende overvågere",
|
||||||
"Verify Token": "Verify Token",
|
"Verify Token": "Verificere Token",
|
||||||
"Setup 2FA": "Setup 2FA",
|
"Setup 2FA": "Opsæt 2FA",
|
||||||
"Enable 2FA": "Enable 2FA",
|
"Enable 2FA": "Aktiver 2FA",
|
||||||
"Disable 2FA": "Disable 2FA",
|
"Disable 2FA": "Deaktiver 2FA",
|
||||||
"2FA Settings": "2FA Settings",
|
"2FA Settings": "2FA Indstillinger",
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
"Two Factor Authentication": "To-Faktor Autentificering",
|
||||||
Active: "Active",
|
Active: "Aktive",
|
||||||
Inactive: "Inactive",
|
Inactive: "Inaktive",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Vis URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Ryd alle Statistikker",
|
||||||
}
|
retryCheckEverySecond: "Prøv igen hvert {0} sekund.",
|
||||||
|
importHandleDescription: "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
|
||||||
|
confirmImportMsg: "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Gentagelsesinterval",
|
||||||
|
"Import Backup": "Importer Backup",
|
||||||
|
"Export Backup": "Eksporter Backup",
|
||||||
|
"Skip existing": "Spring over eksisterende",
|
||||||
|
Overwrite: "Overskriv",
|
||||||
|
Options: "Valgmuligheder",
|
||||||
|
"Keep both": "Behold begge",
|
||||||
|
Tags: "Tags",
|
||||||
|
"Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
|
||||||
|
"Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
|
||||||
|
"Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
|
||||||
|
color: "farve",
|
||||||
|
"value (optional)": "værdi (valgfri)",
|
||||||
|
Gray: "Grå",
|
||||||
|
Red: "Rød",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Grøn",
|
||||||
|
Blue: "Blå",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Lilla",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Søg...",
|
||||||
|
"Avg. Ping": "Gns. Ping",
|
||||||
|
"Avg. Response": "Gns. Respons",
|
||||||
|
"Entry Page": "Entry Side",
|
||||||
|
"statusPageNothing": "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
|
||||||
|
"No Services": "Ingen Tjenester",
|
||||||
|
"All Systems Operational": "Alle Systemer i Drift",
|
||||||
|
"Partially Degraded Service": "Delvist Forringet Service",
|
||||||
|
"Degraded Service": "Forringet Service",
|
||||||
|
"Add Group": "Tilføj Gruppe",
|
||||||
|
"Add a monitor": "Tilføj en Overvåger",
|
||||||
|
"Edit Status Page": "Rediger Statusside",
|
||||||
|
"Go to Dashboard": "Gå til Dashboard",
|
||||||
|
};
|
||||||
|
|
|
@ -36,7 +36,6 @@ export default {
|
||||||
hour: "Stunde",
|
hour: "Stunde",
|
||||||
"-hour": "-Stunden",
|
"-hour": "-Stunden",
|
||||||
checkEverySecond: "Überprüfe alle {0} Sekunden",
|
checkEverySecond: "Überprüfe alle {0} Sekunden",
|
||||||
"Avg.": "Durchschn.",
|
|
||||||
Response: "Antwortzeit",
|
Response: "Antwortzeit",
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Monitor Typ",
|
"Monitor Type": "Monitor Typ",
|
||||||
|
@ -113,7 +112,6 @@ export default {
|
||||||
"Create your admin account": "Erstelle dein Admin Konto",
|
"Create your admin account": "Erstelle dein Admin Konto",
|
||||||
"Repeat Password": "Wiederhole das Passwort",
|
"Repeat Password": "Wiederhole das Passwort",
|
||||||
"Resource Record Type": "Resource Record Type",
|
"Resource Record Type": "Resource Record Type",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
|
||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
respTime: "Antw. Zeit (ms)",
|
respTime: "Antw. Zeit (ms)",
|
||||||
|
@ -128,6 +126,13 @@ export default {
|
||||||
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
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.",
|
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
||||||
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
||||||
|
"Clear all statistics": "Lösche alle Statistiken",
|
||||||
|
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
|
||||||
|
"Skip existing": "Vorhandene überspringen",
|
||||||
|
Overwrite: "Überschreiben",
|
||||||
|
Options: "Optionen",
|
||||||
|
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
|
||||||
|
"Keep both": "Beide behalten",
|
||||||
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
|
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
|
||||||
"Verify Token": "Token verifizieren",
|
"Verify Token": "Token verifizieren",
|
||||||
"Setup 2FA": "2FA Einrichten",
|
"Setup 2FA": "2FA Einrichten",
|
||||||
|
@ -142,5 +147,35 @@ export default {
|
||||||
Inactive: "Inaktiv",
|
Inactive: "Inaktiv",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "URI Anzeigen",
|
"Show URI": "URI Anzeigen",
|
||||||
"Clear all statistics": "Lösche alle Statistiken",
|
Tags: "Tags",
|
||||||
}
|
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...",
|
||||||
|
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.",
|
||||||
|
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.",
|
||||||
|
color: "Farbe",
|
||||||
|
"value (optional)": "Wert (Optional)",
|
||||||
|
Gray: "Grau",
|
||||||
|
Red: "Rot",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Grün",
|
||||||
|
Blue: "Blau",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Lila",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Suchen...",
|
||||||
|
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall",
|
||||||
|
retryCheckEverySecond: "Versuche alle {0} Sekunden",
|
||||||
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
|
"Avg. Ping": "Durchsch. Ping",
|
||||||
|
"Avg. Response": "Durchsch. Antwort",
|
||||||
|
"Entry Page": "Einstiegsseite",
|
||||||
|
statusPageNothing: "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
|
||||||
|
"No Services": "Keine Dienste",
|
||||||
|
"All Systems Operational": "Alle Systeme Betriebsbereit",
|
||||||
|
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
|
||||||
|
"Degraded Service": "Eingeschränkter Dienst",
|
||||||
|
"Add Group": "Gruppe hinzufügen",
|
||||||
|
"Add a monitor": "Monitor hinzufügen",
|
||||||
|
"Edit Status Page": "Bearbeite Statusseite",
|
||||||
|
"Go to Dashboard": "Gehe zum Dashboard",
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "English",
|
languageName: "English",
|
||||||
checkEverySecond: "Check every {0} seconds.",
|
checkEverySecond: "Check every {0} seconds.",
|
||||||
"Avg.": "Avg.",
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
|
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
|
||||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||||
|
@ -20,6 +20,8 @@ export default {
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this 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?",
|
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 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",
|
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||||
|
@ -68,6 +70,7 @@ export default {
|
||||||
Port: "Port",
|
Port: "Port",
|
||||||
"Heartbeat Interval": "Heartbeat Interval",
|
"Heartbeat Interval": "Heartbeat Interval",
|
||||||
Retries: "Retries",
|
Retries: "Retries",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
Advanced: "Advanced",
|
Advanced: "Advanced",
|
||||||
"Upside Down Mode": "Upside Down Mode",
|
"Upside Down Mode": "Upside Down Mode",
|
||||||
"Max. Redirects": "Max. Redirects",
|
"Max. Redirects": "Max. Redirects",
|
||||||
|
@ -115,7 +118,8 @@ export default {
|
||||||
"Last Result": "Last Result",
|
"Last Result": "Last Result",
|
||||||
"Create your admin account": "Create your admin account",
|
"Create your admin account": "Create your admin account",
|
||||||
"Repeat Password": "Repeat Password",
|
"Repeat Password": "Repeat Password",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
respTime: "Resp. Time (ms)",
|
respTime: "Resp. Time (ms)",
|
||||||
|
@ -132,6 +136,11 @@ export default {
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||||
alertNoFile: "Please select a file to import.",
|
alertNoFile: "Please select a file to import.",
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
alertWrongFileType: "Please select a JSON file.",
|
||||||
|
"Clear all statistics": "Clear all Statistics",
|
||||||
|
"Skip existing": "Skip existing",
|
||||||
|
Overwrite: "Overwrite",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Keep both",
|
||||||
"Verify Token": "Verify Token",
|
"Verify Token": "Verify Token",
|
||||||
"Setup 2FA": "Setup 2FA",
|
"Setup 2FA": "Setup 2FA",
|
||||||
"Enable 2FA": "Enable 2FA",
|
"Enable 2FA": "Enable 2FA",
|
||||||
|
@ -142,5 +151,48 @@ export default {
|
||||||
Inactive: "Inactive",
|
Inactive: "Inactive",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
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",
|
||||||
|
"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",
|
||||||
|
"lunasea": "LunaSea",
|
||||||
|
"apprise": "Apprise (Support 50+ Notification services)",
|
||||||
|
"pushbullet": "Pushbullet",
|
||||||
|
"line": "Line Messenger",
|
||||||
|
"mattermost": "Mattermost",
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "Español",
|
languageName: "Español",
|
||||||
checkEverySecond: "Comprobar cada {0} segundos.",
|
checkEverySecond: "Comprobar cada {0} segundos.",
|
||||||
"Avg.": "Media.",
|
|
||||||
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
|
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
|
||||||
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
|
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
|
||||||
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
|
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
|
||||||
|
@ -32,7 +31,7 @@ export default {
|
||||||
Up: "Funcional",
|
Up: "Funcional",
|
||||||
Down: "Caído",
|
Down: "Caído",
|
||||||
Pending: "Pendiente",
|
Pending: "Pendiente",
|
||||||
Unknown: "Desconociso",
|
Unknown: "Desconocido",
|
||||||
Pause: "Pausa",
|
Pause: "Pausa",
|
||||||
Name: "Nombre",
|
Name: "Nombre",
|
||||||
Status: "Estado",
|
Status: "Estado",
|
||||||
|
@ -120,7 +119,6 @@ export default {
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
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",
|
"Default enabled": "Default enabled",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
|
||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||||
|
@ -144,4 +142,41 @@ export default {
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Clear all Statistics",
|
||||||
}
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
|
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||||
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
|
"Skip existing": "Skip existing",
|
||||||
|
Overwrite: "Overwrite",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Keep both",
|
||||||
|
Tags: "Tags",
|
||||||
|
"Add New below or Select...": "Add New below or Select...",
|
||||||
|
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||||
|
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||||
|
color: "color",
|
||||||
|
"value (optional)": "value (optional)",
|
||||||
|
Gray: "Gray",
|
||||||
|
Red: "Red",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Green",
|
||||||
|
Blue: "Blue",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Purple",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Avg. Ping": "Avg. Ping",
|
||||||
|
"Avg. Response": "Avg. Response",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "eesti",
|
languageName: "eesti",
|
||||||
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
|
retryCheckEverySecond: "Kontrolli {0} sekundilise vahega.",
|
||||||
"Avg.": "≈",
|
|
||||||
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
|
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
|
||||||
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
|
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
|
||||||
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
|
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
|
||||||
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
|
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
|
||||||
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
|
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
|
||||||
passwordNotMatchMsg: "Salasõnad ei kattu.",
|
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)",
|
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
|
||||||
pauseDashboardHome: "Seismas",
|
pauseDashboardHome: "Seisatud",
|
||||||
deleteMonitorMsg: "Kas soovid eemaldada seire?",
|
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.",
|
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
|
||||||
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
|
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
|
||||||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||||
Settings: "Seaded",
|
Settings: "Seaded",
|
||||||
|
"Status Page": "Ülevaade", // hääletuse tulemus, teine: seisundileht, kolmas: Olukord/Olek
|
||||||
Dashboard: "Töölaud",
|
Dashboard: "Töölaud",
|
||||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||||
Language: "Keel",
|
Language: "Keel",
|
||||||
|
@ -27,30 +27,31 @@ export default {
|
||||||
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
|
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
|
||||||
List: "Nimekiri",
|
List: "Nimekiri",
|
||||||
Add: "Lisa",
|
Add: "Lisa",
|
||||||
"Add New Monitor": "Seire lisamine",
|
"Add New Monitor": "Lisa seire",
|
||||||
|
"Add a monitor": "Lisa seire",
|
||||||
"Quick Stats": "Ülevaade",
|
"Quick Stats": "Ülevaade",
|
||||||
Up: "Töökorras",
|
Up: "Töökorras",
|
||||||
Down: "Rikkis",
|
Down: "Rikkis",
|
||||||
Pending: "Määramisel",
|
Pending: "Määramisel",
|
||||||
Unknown: "Teadmata",
|
Unknown: "Kahtlast",
|
||||||
Pause: "Seiskamine",
|
Pause: "Seiska",
|
||||||
Name: "Nimi",
|
Name: "Nimi",
|
||||||
Status: "Olek",
|
Status: "Olek",
|
||||||
DateTime: "Kuupäev",
|
DateTime: "Kuupäev",
|
||||||
Message: "Tulemus",
|
Message: "Tulemus",
|
||||||
"No important events": "Märkimisväärsed juhtumid puuduvad.",
|
"No important events": "Märkimisväärsed juhtumid puuduvad.",
|
||||||
Resume: "Taasta",
|
Resume: "Taasta",
|
||||||
Edit: "Muutmine",
|
Edit: "Muuda",
|
||||||
Delete: "Eemalda",
|
Delete: "Eemalda",
|
||||||
Current: "Hetkeseisund",
|
Current: "Hetkeseisund",
|
||||||
Uptime: "Eluiga",
|
Uptime: "Eluiga", // todo: launchpad?
|
||||||
"Cert Exp.": "Sert. aegumine",
|
"Cert Exp.": "Sert. aegumine",
|
||||||
days: "päeva",
|
days: "päeva",
|
||||||
day: "päev",
|
day: "päev",
|
||||||
"-day": "-päev",
|
"-day": "-päev",
|
||||||
hour: "tund",
|
hour: "tund",
|
||||||
"-hour": "-tund",
|
"-hour": "-tund",
|
||||||
Response: "Vastus",
|
Response: "Reaktsiooniaeg",
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Seire tüüp",
|
"Monitor Type": "Seire tüüp",
|
||||||
Keyword: "Võtmesõna",
|
Keyword: "Võtmesõna",
|
||||||
|
@ -108,40 +109,73 @@ export default {
|
||||||
"Create your admin account": "Admininstraatori konto loomine",
|
"Create your admin account": "Admininstraatori konto loomine",
|
||||||
"Repeat Password": "korda salasõna",
|
"Repeat Password": "korda salasõna",
|
||||||
respTime: "Reageerimisaeg (ms)",
|
respTime: "Reageerimisaeg (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A", // tõlkimata, umbkaudu rahvusvaheline termin peaks sobima piisavalt
|
||||||
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?",
|
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
|
||||||
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
|
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
|
||||||
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
|
confirmClearStatisticsMsg: "Kas soovid TERVE ajaloo kustutada?",
|
||||||
"Import/Export Backup": "Impordi/Ekspordi varukoopia",
|
|
||||||
Export: "Eksport",
|
Export: "Eksport",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
"Default enabled": "Kasuta vaikimisi",
|
"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",
|
Create: "Loo konto",
|
||||||
"Clear Data": "Eemalda andmed",
|
"Clear Data": "Eemalda andmed",
|
||||||
Events: "Sündmused",
|
Events: "Sündmused",
|
||||||
Heartbeats: "Tuksed",
|
Heartbeats: "Tuksed",
|
||||||
"Auto Get": "Hangi automaatselt",
|
"Auto Get": "Hangi automaatselt", // hangi? kõlab liiga otsetõlge
|
||||||
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
|
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
|
||||||
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
|
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.",
|
alertNoFile: "Palun lisa fail, mida importida.",
|
||||||
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
|
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
twoFAVerifyLabel: "2FA kinnitamiseks sisesta pääsukood",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
tokenValidSettingsMsg: "Kood õige. Akna võib sulgeda.",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
confirmEnableTwoFAMsg: "Kas soovid 2FA sisse lülitada?",
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
confirmDisableTwoFAMsg: "Kas soovid 2FA välja lülitada?",
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
"Verify Token": "Kontrolli",
|
||||||
"Verify Token": "Verify Token",
|
"Setup 2FA": "Kaksikautentimise seadistamine",
|
||||||
"Setup 2FA": "Setup 2FA",
|
"Enable 2FA": "Seadista 2FA",
|
||||||
"Enable 2FA": "Enable 2FA",
|
"Disable 2FA": "Lülita 2FA välja",
|
||||||
"Disable 2FA": "Disable 2FA",
|
"2FA Settings": "2FA seaded",
|
||||||
"2FA Settings": "2FA Settings",
|
"Two Factor Authentication": "Kaksikautentimine",
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
Active: "kasutusel",
|
||||||
Active: "Active",
|
Inactive: "seadistamata",
|
||||||
Inactive: "Inactive",
|
Token: "kaksikautentimise kood", // needs to compensate for no title
|
||||||
Token: "Token",
|
"Show URI": "Näita URId",
|
||||||
"Show URI": "Show URI",
|
"Clear all statistics": "Tühjenda ajalugu",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
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", // reusal of key would be chaos
|
||||||
|
"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)", // milline sõna!
|
||||||
|
Gray: "hall",
|
||||||
|
Red: "punane",
|
||||||
|
Orange: "oranž",
|
||||||
|
Green: "roheline",
|
||||||
|
Blue: "sinine",
|
||||||
|
Indigo: "indigo",
|
||||||
|
Purple: "lilla",
|
||||||
|
Pink: "roosa",
|
||||||
|
"Search...": "Otsi…",
|
||||||
|
"Avg. Ping": "Keskmine ping", // pikk, aga nagunii kahel real
|
||||||
|
"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",
|
||||||
|
};
|
||||||
|
|
190
src/languages/fa.js
Normal file
190
src/languages/fa.js
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
export default {
|
||||||
|
languageName: "Farsi",
|
||||||
|
checkEverySecond: "بررسی هر {0} ثانیه.",
|
||||||
|
retryCheckEverySecond: "تکرار مجدد هر {0} ثانیه.",
|
||||||
|
retriesDescription: "حداکثر تعداد تکرار پیش از علامت گذاری وبسایت بعنوان خارج از دسترس و ارسال اطلاعرسانی.",
|
||||||
|
ignoreTLSError: "بیخیال ارور TLS/SSL برای سایتهای HTTPS",
|
||||||
|
upsideDownModeDescription: "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است!",
|
||||||
|
maxRedirectDescription: "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ریدایرکتها پشتیبانی نشوند، عدد 0 را وارد کنید.",
|
||||||
|
acceptedStatusCodesDescription: "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.",
|
||||||
|
passwordNotMatchMsg: "تکرار رمز عبور مطابقت ندارد!",
|
||||||
|
notificationDescription: "برای اینکه سرویس اطلاعرسانی کار کند، آنرا به یکی از مانیتورها متصل کنید.",
|
||||||
|
keywordDescription: "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).",
|
||||||
|
pauseDashboardHome: "متوقف شده",
|
||||||
|
deleteMonitorMsg: "آیا از حذف این مانیتور مطمئن هستید؟",
|
||||||
|
deleteNotificationMsg: "آیا مطمئن هستید که میخواهید این سرویس اطلاعرسانی را برای تمامی مانیتورها حذف کنید؟",
|
||||||
|
resoverserverDescription: "سرویس CloudFlare به عنوان سرور پیشفرض استفاده میشود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
|
||||||
|
rrtypeDescription: "لطفا نوع Resource Record را انتخاب کنید.",
|
||||||
|
pauseMonitorMsg: "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟",
|
||||||
|
enableDefaultNotificationDescription: "برای هر مانیتور جدید، این سرویس اطلاعرسانی به صورت پیشفرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.",
|
||||||
|
clearEventsMsg: "آیا از اینکه تمامی تاریخچه رویدادهای این مانیتور حذف شود مطمئن هستید؟",
|
||||||
|
clearHeartbeatsMsg: "آیا از اینکه تاریخچه تمامی Heartbeat های این مانیتور حذف شود مطمئن هستید؟ ",
|
||||||
|
confirmClearStatisticsMsg: "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟",
|
||||||
|
importHandleDescription: " اگر که میخواهید بیخیال مانیتورها و یا سرویسهای اطلاعرسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بیخیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.",
|
||||||
|
confirmImportMsg: "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کردهاید اطمینان حاصل کنید!",
|
||||||
|
twoFAVerifyLabel: "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحلهای توکن خود را وارد کنید!",
|
||||||
|
tokenValidSettingsMsg: "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحلهای را فعال کنید!",
|
||||||
|
confirmEnableTwoFAMsg: " آیا از فعال سازی احراز هویت دو مرحلهای مطمئن هستید؟",
|
||||||
|
confirmDisableTwoFAMsg: "آیا از غیرفعال سازی احراز هویت دومرحلهای مطمئن هستید؟",
|
||||||
|
Settings: "تنظیمات",
|
||||||
|
Dashboard: "پیشخوان",
|
||||||
|
"New Update": "بروزرسانی جدید!",
|
||||||
|
Language: "زبان",
|
||||||
|
Appearance: "ظاهر",
|
||||||
|
Theme: "پوسته",
|
||||||
|
General: "عمومی",
|
||||||
|
Version: "نسخه",
|
||||||
|
"Check Update On 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.": "تاریخ انقضای SSL",
|
||||||
|
days: "روز",
|
||||||
|
day: "روز",
|
||||||
|
"-day": "-روز",
|
||||||
|
hour: "ساعت",
|
||||||
|
"-hour": "-ساعت",
|
||||||
|
Response: "پاسخ",
|
||||||
|
Ping: "Ping",
|
||||||
|
"Monitor Type": "نوع مانیتور",
|
||||||
|
Keyword: "کلمه کلیدی",
|
||||||
|
"Friendly Name": "عنوان",
|
||||||
|
URL: "آدرس (URL)",
|
||||||
|
Hostname: "نام میزبان (Hostname)",
|
||||||
|
Port: "پورت",
|
||||||
|
"Heartbeat Interval": "فاصله هر Heartbeat",
|
||||||
|
Retries: "تلاش مجدد",
|
||||||
|
"Heartbeat Retry Interval": "فاصله تلاش مجدد برایHeartbeat",
|
||||||
|
Advanced: "پیشرفته",
|
||||||
|
"Upside Down Mode": "حالت بر عکس",
|
||||||
|
"Max. Redirects": "حداکثر تعداد ریدایرکت",
|
||||||
|
"Accepted Status Codes": "وضعیتهای (Status Code) های قابل قبول",
|
||||||
|
Save: "ذخیره",
|
||||||
|
Notifications: "اطلاعرسانیها",
|
||||||
|
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید!",
|
||||||
|
"Setup Notification": "راه اندازی اطلاعرسانی",
|
||||||
|
Light: "روشن",
|
||||||
|
Dark: "تاریک",
|
||||||
|
Auto: "اتوماتیک",
|
||||||
|
"Theme - Heartbeat Bar": "ظاهر نوار Heartbeat",
|
||||||
|
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": "سرور Resolver",
|
||||||
|
"Resource Record Type": "نوع رکورد (Resource Record Type)",
|
||||||
|
"Last Result": "آخرین نتیجه",
|
||||||
|
"Create your admin account": "ایجاد حساب کاربری مدیر",
|
||||||
|
"Repeat Password": "تکرار رمز عبور",
|
||||||
|
"Import Backup": "بازگردانی فایل پشتیبان",
|
||||||
|
"Export Backup": "ذخیره فایل پشتیبان",
|
||||||
|
Export: "استخراج اطلاعات",
|
||||||
|
Import: "ورود اطلاعات",
|
||||||
|
respTime: "زمان پاسخگویی (میلیثانیه)",
|
||||||
|
notAvailableShort: "ناموجود",
|
||||||
|
"Default enabled": "به صورت پیشفرض فعال باشد.",
|
||||||
|
"Apply on all existing monitors": "بر روی تمامی مانیتورهای فعلی اعمال شود.",
|
||||||
|
Create: "ایجاد",
|
||||||
|
"Clear Data": "پاکسازی دادهها",
|
||||||
|
Events: "رخدادها",
|
||||||
|
Heartbeats: "Heartbeats",
|
||||||
|
"Auto Get": "Auto Get",
|
||||||
|
backupDescription: "شما میتوانید تمامی مانیتورها و تنظیمات اطلاعرسانیها را در قالب یه فایل JSON دریافت کنید.",
|
||||||
|
backupDescription2: "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.",
|
||||||
|
backupDescription3: "توجه داشته باشید که تمامی اطلاعات حساس شما مانند توکنها نیز در این فایل وجود خواهد داشت ، پس از این فایل به خوبی مراقبت کنید.",
|
||||||
|
alertNoFile: "لطفا یک فایل برای «ورود اطلاعات» انتخاب کنید..",
|
||||||
|
alertWrongFileType: "یک فایل JSON انتخاب کنید.",
|
||||||
|
"Clear all statistics": "پاکسازی تمامی آمار و ارقام",
|
||||||
|
"Skip existing": "بیخیال مواردی که از قبل موجود است",
|
||||||
|
Overwrite: "بازنویسی",
|
||||||
|
Options: "تنظیمات",
|
||||||
|
"Keep both": "هر دو را نگه دار",
|
||||||
|
"Verify Token": "تایید توکن",
|
||||||
|
"Setup 2FA": "تنظیمات احراز دو مرحلهای",
|
||||||
|
"Enable 2FA": "فعال سازی احراز 2 مرحلهای",
|
||||||
|
"Disable 2FA": "غیر فعال کردن احراز 2 مرحلهای",
|
||||||
|
"2FA Settings": "تنظیمات احراز 2 مرحلهای",
|
||||||
|
"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": "ویرایش صفحه وضعیت",
|
||||||
|
"Status Page": "صفحه وضعیت",
|
||||||
|
"Go to Dashboard": "رفتن به پیشخوان",
|
||||||
|
"Uptime Kuma": "آپتایم کوما",
|
||||||
|
records: "مورد",
|
||||||
|
"One record": "یک مورد",
|
||||||
|
"Showing {from} to {to} of {count} records": "نمایش از {from} تا {to} از {count} مورد",
|
||||||
|
First: "اولین",
|
||||||
|
Last: "آخرین",
|
||||||
|
Info: "اطلاعات",
|
||||||
|
"Powered By": "نیرو گرفته از",
|
||||||
|
};
|
|
@ -36,7 +36,6 @@ export default {
|
||||||
hour: "Heure",
|
hour: "Heure",
|
||||||
"-hour": "Heures",
|
"-hour": "Heures",
|
||||||
checkEverySecond: "Vérifier toutes les {0} secondes",
|
checkEverySecond: "Vérifier toutes les {0} secondes",
|
||||||
"Avg.": "Moyen",
|
|
||||||
Response: "Temps de réponse",
|
Response: "Temps de réponse",
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Type de Sonde",
|
"Monitor Type": "Type de Sonde",
|
||||||
|
@ -110,38 +109,74 @@ export default {
|
||||||
respTime: "Temps de réponse (ms)",
|
respTime: "Temps de réponse (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
Create: "Créer",
|
Create: "Créer",
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
clearEventsMsg: "Êtes-vous sûr de vouloir supprimer tous les événements pour cette sonde ?",
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
clearHeartbeatsMsg: "Êtes-vous sûr de vouloir supprimer tous les vérifications pour cette sonde ? Are you sure want to delete all heartbeats for this monitor?",
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
confirmClearStatisticsMsg: "tes-vous sûr de vouloir supprimer tous les statistiques ?",
|
||||||
"Clear Data": "Clear Data",
|
"Clear Data": "Effacer les données",
|
||||||
Events: "Events",
|
Events: "Evénements",
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Vérfications",
|
||||||
"Auto Get": "Auto Get",
|
"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.",
|
enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Activé par défaut",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "S'applique également aux sondes existantes",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
Export: "Exporter",
|
||||||
Export: "Export",
|
Import: "Importer",
|
||||||
Import: "Import",
|
backupDescription: "Vous pouvez sauvegarder toutes les sondes et toutes les notifications dans un fichier JSON.",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription2: "PS: Les données relatives à l'historique et aux événements ne sont pas incluses.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
backupDescription3: "Les données sensibles telles que les jetons de notification sont incluses dans le fichier d'exportation, veuillez les conserver soigneusement.",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
alertNoFile: "Veuillez sélectionner un fichier à importer.",
|
||||||
alertNoFile: "Please select a file to import.",
|
alertWrongFileType: "Veuillez sélectionner un fichier JSON à importer.",
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
twoFAVerifyLabel: "Veuillez saisir votre jeton pour vérifier que le système 2FA fonctionne.",
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
tokenValidSettingsMsg: "Le jeton est valide ! Vous pouvez maintenant sauvegarder les paramètres 2FA.",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
confirmEnableTwoFAMsg: "Êtes-vous sûr de vouloir activer le 2FA ?",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
confirmDisableTwoFAMsg: "Êtes-vous sûr de vouloir désactiver le 2FA ?",
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
"Apply on all existing monitors": "Appliquer sur toutes les sondes existantes",
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
"Verify Token": "Vérifier le jeton",
|
||||||
"Verify Token": "Verify Token",
|
"Setup 2FA": "Configurer 2FA",
|
||||||
"Setup 2FA": "Setup 2FA",
|
"Enable 2FA": "Activer 2FA",
|
||||||
"Enable 2FA": "Enable 2FA",
|
"Disable 2FA": "Désactiver 2FA",
|
||||||
"Disable 2FA": "Disable 2FA",
|
"2FA Settings": "Paramètres 2FA",
|
||||||
"2FA Settings": "2FA Settings",
|
"Two Factor Authentication": "Authentification à deux facteurs",
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
Active: "Actif",
|
||||||
Active: "Active",
|
Inactive: "Inactif",
|
||||||
Inactive: "Inactive",
|
Token: "Jeton",
|
||||||
Token: "Token",
|
"Show URI": "Afficher l'URI",
|
||||||
"Show URI": "Show URI",
|
"Clear all statistics": "Effacer touutes les statistiques",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
retryCheckEverySecond: "Réessayer toutes les {0} secondes.",
|
||||||
}
|
importHandleDescription: "Choisissez 'Ignorer l'existant' si vous voulez ignorer chaque sonde ou notification portant le même nom. L'option 'Écraser' supprime tous les sondes et notifications existantes.",
|
||||||
|
confirmImportMsg: "Êtes-vous sûr d'importer la sauvegarde ? Veuillez vous assurer que vous avez sélectionné la bonne option d'importation.",
|
||||||
|
"Heartbeat Retry Interval": "Réessayer l'intervale de vérification",
|
||||||
|
"Import Backup": "Importation de la sauvegarde",
|
||||||
|
"Export Backup": "Exportation de la sauvegarde",
|
||||||
|
"Skip existing": "Sauter l'existant",
|
||||||
|
Overwrite: "Ecraser",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Garder les deux",
|
||||||
|
Tags: "Étiquettes",
|
||||||
|
"Add New below or Select...": "Ajouter nouveau ci-dessous ou sélectionner...",
|
||||||
|
"Tag with this name already exist.": "Une étiquette portant ce nom existe déjà.",
|
||||||
|
"Tag with this value already exist.": "Une étiquette avec cette valeur existe déjà.",
|
||||||
|
color: "couleur",
|
||||||
|
"value (optional)": "valeur (facultatif)",
|
||||||
|
Gray: "Gris",
|
||||||
|
Red: "Rouge",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Vert",
|
||||||
|
Blue: "Bleu",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Violet",
|
||||||
|
Pink: "Rose",
|
||||||
|
"Search...": "Rechercher...",
|
||||||
|
"Avg. Ping": "Ping moyen",
|
||||||
|
"Avg. Response": "Réponse moyenne",
|
||||||
|
"Entry Page": "Page d'accueil",
|
||||||
|
"statusPageNothing": "Rien ici, veuillez ajouter un groupe ou une sonde.",
|
||||||
|
"No Services": "Aucun service",
|
||||||
|
"All Systems Operational": "Tous les systèmes sont opérationnels",
|
||||||
|
"Partially Degraded Service": "Service partiellement dégradé",
|
||||||
|
"Degraded Service": "Service dégradé",
|
||||||
|
"Add Group": "Ajouter un groupe",
|
||||||
|
"Add a monitor": "Ajouter une sonde",
|
||||||
|
"Edit Status Page": "Modifier la page de statut",
|
||||||
|
"Go to Dashboard": "Accéder au tableau de bord",
|
||||||
|
};
|
||||||
|
|
181
src/languages/hu.js
Normal file
181
src/languages/hu.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
export default {
|
||||||
|
languageName: "Magyar",
|
||||||
|
checkEverySecond: "Ellenőrzés {0} másodpercenként",
|
||||||
|
retryCheckEverySecond: "Újrapróbál {0} másodpercenként.",
|
||||||
|
retriesDescription: "Maximális próbálkozás mielőtt a szolgáltatás leállt jelőlést kap és értesítés kerül kiküldésre",
|
||||||
|
ignoreTLSError: "TLS/SSL hibák figyelnen kívül hagyása HTTPS weboldalaknál",
|
||||||
|
upsideDownModeDescription: "Az állapot megfordítása. Ha a szolgáltatás elérhető, akkor lesz leállt állapotú.",
|
||||||
|
maxRedirectDescription: "Az átirányítások maximális száma. állítsa 0-ra az átirányítás tiltásához.",
|
||||||
|
acceptedStatusCodesDescription: "Válassza ki az állapot kódokat amelyek sikeres válasznak fognak számítani.",
|
||||||
|
passwordNotMatchMsg: "A megismételt jelszó nem egyezik.",
|
||||||
|
notificationDescription: "Kérem, rendeljen egy értesítést a figyeléshez, hogy működjön.",
|
||||||
|
keywordDescription: "Kulcsszó keresése a html-ben vagy a JSON válaszban. (kis-nagybetű érzékeny)",
|
||||||
|
pauseDashboardHome: "Szünetel",
|
||||||
|
deleteMonitorMsg: "Biztos, hogy törölni akarja ezt a figyelőt?",
|
||||||
|
deleteNotificationMsg: "Biztos, hogy törölni akarja ezt az értesítést az összes figyelőnél?",
|
||||||
|
resoverserverDescription: "A Cloudflare az alapértelmezett szerver, bármikor meg tudja változtatni a resolver server-t.",
|
||||||
|
rrtypeDescription: "Válassza ki az RR-Típust a figyelőhöz",
|
||||||
|
pauseMonitorMsg: "Biztos, hogy szüneteltetni akarja?",
|
||||||
|
enableDefaultNotificationDescription: "Minden új figyelőhöz ez az értesítés engedélyezett lesz alapértelmezetten. Kikapcsolhatja az értesítést külön minden figyelőnél.",
|
||||||
|
clearEventsMsg: "Biztos, hogy törölni akar miden eseményt ennél a figyelnél?",
|
||||||
|
clearHeartbeatsMsg: "Biztos, hogy törölni akar minden heartbeat-et ennél a figyelőnél?",
|
||||||
|
confirmClearStatisticsMsg: "Biztos, hogy törölni akat MINDEN statisztikát?",
|
||||||
|
importHandleDescription: "Válassza a 'Meglévő kihagyását', ha ki szeretné hagyni az azonos nevő figyelőket vagy értesítésket. A 'Felülírás' törölni fog minden meglévő figyelőt és értesítést.",
|
||||||
|
confirmImportMsg: "Biztos, hogy importálja a mentést? Győzödjön meg róla, hogy jól választotta ki az importálás opciót.",
|
||||||
|
twoFAVerifyLabel: "Kérem, adja meg a token-t, hogy a 2FA működését ellenőrizzük",
|
||||||
|
tokenValidSettingsMsg: "A token érvényes! El tudja menteni a 2FA beállításait.",
|
||||||
|
confirmEnableTwoFAMsg: "Biztosan engedélyezi a 2FA-t?",
|
||||||
|
confirmDisableTwoFAMsg: "Biztosan letiltja a 2FA-t?",
|
||||||
|
Settings: "Beállítások",
|
||||||
|
Dashboard: "Irányítópult",
|
||||||
|
"New Update": "Új frissítés",
|
||||||
|
Language: "Nyelv",
|
||||||
|
Appearance: "Megjelenés",
|
||||||
|
Theme: "Téma",
|
||||||
|
General: "Általános",
|
||||||
|
Version: "Verzió",
|
||||||
|
"Check Update On GitHub": "Frissítések keresése a GitHub-on",
|
||||||
|
List: "Lista",
|
||||||
|
Add: "Hozzáadás",
|
||||||
|
"Add New Monitor": "Új figyelő hozzáadása",
|
||||||
|
"Quick Stats": "Gyors statisztikák",
|
||||||
|
Up: "Működik",
|
||||||
|
Down: "Leállt",
|
||||||
|
Pending: "Függőben",
|
||||||
|
Unknown: "Ismeretlen",
|
||||||
|
Pause: "Szünet",
|
||||||
|
Name: "Név",
|
||||||
|
Status: "Állapot",
|
||||||
|
DateTime: "Időpont",
|
||||||
|
Message: "Üzenet",
|
||||||
|
"No important events": "Nincs fontos esemény",
|
||||||
|
Resume: "Folytatás",
|
||||||
|
Edit: "Szerkesztés",
|
||||||
|
Delete: "Törlés",
|
||||||
|
Current: "Aktuális",
|
||||||
|
Uptime: "Uptime",
|
||||||
|
"Cert Exp.": "Tanúsítvány lejár",
|
||||||
|
days: "napok",
|
||||||
|
day: "nap",
|
||||||
|
"-day": "-nap",
|
||||||
|
hour: "óra",
|
||||||
|
"-hour": "-óra",
|
||||||
|
Response: "Válasz",
|
||||||
|
Ping: "Ping",
|
||||||
|
"Monitor Type": "Figyelő típusa",
|
||||||
|
Keyword: "Kulcsszó",
|
||||||
|
"Friendly Name": "Rövid név",
|
||||||
|
URL: "URL",
|
||||||
|
Hostname: "Hostnév",
|
||||||
|
Port: "Port",
|
||||||
|
"Heartbeat Interval": "Heartbeat időköz",
|
||||||
|
Retries: "Újrapróbálkozás",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat újrapróbálkozások időköze",
|
||||||
|
Advanced: "Haladó",
|
||||||
|
"Upside Down Mode": "Fordított mód",
|
||||||
|
"Max. Redirects": "Max. átirányítás",
|
||||||
|
"Accepted Status Codes": "Elfogadott állapot kódok",
|
||||||
|
Save: "Mentés",
|
||||||
|
Notifications: "Értesítések",
|
||||||
|
"Not available, please setup.": "Nem elérhető, állítsa be.",
|
||||||
|
"Setup Notification": "Értesítés beállítása",
|
||||||
|
Light: "Világos",
|
||||||
|
Dark: "Sötét",
|
||||||
|
Auto: "Auto",
|
||||||
|
"Theme - Heartbeat Bar": "Téma - Heartbeat Bar",
|
||||||
|
Normal: "Normal",
|
||||||
|
Bottom: "Nyomógomb",
|
||||||
|
None: "Nincs",
|
||||||
|
Timezone: "Időzóna",
|
||||||
|
"Search Engine Visibility": "Látható a keresőmotoroknak",
|
||||||
|
"Allow indexing": "Indexelés engedélyezése",
|
||||||
|
"Discourage search engines from indexing site": "Keresőmotorok elriasztása az oldal indexelésétől",
|
||||||
|
"Change Password": "Jelszó változtatása",
|
||||||
|
"Current Password": "Jelenlegi jelszó",
|
||||||
|
"New Password": "Új jelszó",
|
||||||
|
"Repeat New Password": "Ismételje meg az új jelszót",
|
||||||
|
"Update Password": "Jelszó módosítása",
|
||||||
|
"Disable Auth": "Hitelesítés tiltása",
|
||||||
|
"Enable Auth": "Hitelesítés engedélyezése",
|
||||||
|
Logout: "Kijelenetkezés",
|
||||||
|
Leave: "Elhagy",
|
||||||
|
"I understand, please disable": "Megértettem, kérem tilsa le",
|
||||||
|
Confirm: "Megerősítés",
|
||||||
|
Yes: "Igen",
|
||||||
|
No: "Nem",
|
||||||
|
Username: "Felhasználónév",
|
||||||
|
Password: "Jelszó",
|
||||||
|
"Remember me": "Emlékezzen rám",
|
||||||
|
Login: "Bejelentkezés",
|
||||||
|
"No Monitors, please": "Nincs figyelő, kérem",
|
||||||
|
"add one": "adjon hozzá egyet",
|
||||||
|
"Notification Type": "Értesítés típusa",
|
||||||
|
Email: "Email",
|
||||||
|
Test: "Teszt",
|
||||||
|
"Certificate Info": "Tanúsítvány információk",
|
||||||
|
"Resolver Server": "Resolver szerver",
|
||||||
|
"Resource Record Type": "Resource Record típusa",
|
||||||
|
"Last Result": "Utolsó eredmény",
|
||||||
|
"Create your admin account": "Hozza létre az adminisztrátor felhasználót",
|
||||||
|
"Repeat Password": "Jelszó ismétlése",
|
||||||
|
"Import Backup": "Mentés importálása",
|
||||||
|
"Export Backup": "Mentés exportálása",
|
||||||
|
Export: "Exportálás",
|
||||||
|
Import: "Importálás",
|
||||||
|
respTime: "Válaszidő (ms)",
|
||||||
|
notAvailableShort: "N/A",
|
||||||
|
"Default enabled": "Alapértelmezetten engedélyezett",
|
||||||
|
"Apply on all existing monitors": "Alkalmazza az összes figyelőre",
|
||||||
|
Create: "Létrehozás",
|
||||||
|
"Clear Data": "Adatok törlése",
|
||||||
|
Events: "Események",
|
||||||
|
Heartbeats: "Heartbeats",
|
||||||
|
"Auto Get": "Auto Get",
|
||||||
|
backupDescription: "Ki tudja menteni az összes figyelőt és értesítést egy JSON fájlba.",
|
||||||
|
backupDescription2: "Ui.: Történeti és esemény adatokat nem tartalmaz.",
|
||||||
|
backupDescription3: "Érzékeny adatok, pl. szolgáltatás kulcsok is vannak az export fájlban. Figyelmesen őrizze!",
|
||||||
|
alertNoFile: "Válaszzon ki egy fájlt az importáláshoz.",
|
||||||
|
alertWrongFileType: "Válasszon egy JSON fájlt.",
|
||||||
|
"Clear all statistics": "Összes statisztika törlése",
|
||||||
|
"Skip existing": "Meglévő kihagyása",
|
||||||
|
Overwrite: "Felülírás",
|
||||||
|
Options: "Opciók",
|
||||||
|
"Keep both": "Mindegyiket tartsa meg",
|
||||||
|
"Verify Token": "Token ellenőrzése",
|
||||||
|
"Setup 2FA": "2FA beállítása",
|
||||||
|
"Enable 2FA": "2FA engedélyezése",
|
||||||
|
"Disable 2FA": "2FA toltása",
|
||||||
|
"2FA Settings": "2FA beállítások",
|
||||||
|
"Two Factor Authentication": "Two Factor Authentication",
|
||||||
|
Active: "Aktív",
|
||||||
|
Inactive: "Inaktív",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "URI megmutatása",
|
||||||
|
Tags: "Cimkék",
|
||||||
|
"Add New below or Select...": "Adjon hozzá lentre vagy válasszon...",
|
||||||
|
"Tag with this name already exist.": "Ilyen nevű cimke már létezik.",
|
||||||
|
"Tag with this value already exist.": "Ilyen értékű cimke már létezik.",
|
||||||
|
color: "szín",
|
||||||
|
"value (optional)": "érték (opcionális)",
|
||||||
|
Gray: "Szürke",
|
||||||
|
Red: "Piros",
|
||||||
|
Orange: "Narancs",
|
||||||
|
Green: "Zöld",
|
||||||
|
Blue: "Kék",
|
||||||
|
Indigo: "Indigó",
|
||||||
|
Purple: "Lila",
|
||||||
|
Pink: "Rózsaszín",
|
||||||
|
"Search...": "Keres...",
|
||||||
|
"Avg. Ping": "Átl. ping",
|
||||||
|
"Avg. Response": "Átl. válasz",
|
||||||
|
"Entry Page": "Nyitólap",
|
||||||
|
statusPageNothing: "Semmi nincs itt, kérem, adjon hozzá egy figyelőt.",
|
||||||
|
"No Services": "Nincs szolgáltatás",
|
||||||
|
"All Systems Operational": "Minden rendszer működik",
|
||||||
|
"Partially Degraded Service": "Részlegesen leállt szolgáltatás",
|
||||||
|
"Degraded Service": "Leállt szolgáltatás",
|
||||||
|
"Add Group": "Csoport hozzáadása",
|
||||||
|
"Add a monitor": "Figyelő hozzáadása",
|
||||||
|
"Edit Status Page": "Sátusz oldal szerkesztése",
|
||||||
|
"Go to Dashboard": "Menj az irányítópulthoz",
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "Italiano (Italian)",
|
languageName: "Italiano (Italian)",
|
||||||
checkEverySecond: "controlla ogni {0} secondi",
|
checkEverySecond: "controlla ogni {0} secondi",
|
||||||
"Avg.": "Media",
|
retryCheckEverySecond: "Riprova ogni {0} secondi.",
|
||||||
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
|
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
|
||||||
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
|
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
|
||||||
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
|
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
|
||||||
|
@ -16,9 +16,16 @@ export default {
|
||||||
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
|
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
|
||||||
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
|
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
|
||||||
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
|
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
|
||||||
|
enableDefaultNotificationDescription: "Per ogni nuovo monitoraggio questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica separatamente per ogni monitoraggio.",
|
||||||
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
|
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
|
||||||
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
|
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
|
||||||
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
|
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
|
||||||
|
importHandleDescription: "Selezionare 'Ignora gli esistenti' si vuole ignorare l'importazione dei monitoraggi o delle notifiche con lo stesso nome. 'Sovrascrivi' eliminerà ogni monitoraggio e notifica esistente.",
|
||||||
|
confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.",
|
||||||
|
twoFAVerifyLabel: "Scrivi il token per verificare che l'autenticazione a due fattori funzioni",
|
||||||
|
tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.",
|
||||||
|
confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?",
|
||||||
|
confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?",
|
||||||
Settings: "Impostazioni",
|
Settings: "Impostazioni",
|
||||||
Dashboard: "Cruscotto",
|
Dashboard: "Cruscotto",
|
||||||
"New Update": "Nuovo Aggiornamento Disponibile",
|
"New Update": "Nuovo Aggiornamento Disponibile",
|
||||||
|
@ -63,9 +70,10 @@ export default {
|
||||||
Port: "Porta",
|
Port: "Porta",
|
||||||
"Heartbeat Interval": "Intervallo di controllo",
|
"Heartbeat Interval": "Intervallo di controllo",
|
||||||
Retries: "Tentativi",
|
Retries: "Tentativi",
|
||||||
|
"Heartbeat Retry Interval": "Intervallo tra un tentativo di controllo e l'altro",
|
||||||
Advanced: "Avanzate",
|
Advanced: "Avanzate",
|
||||||
"Upside Down Mode": "Modalità capovolta",
|
"Upside Down Mode": "Modalità capovolta",
|
||||||
"Max. Redirects": "Redirezionamenti massimi",
|
"Max. Redirects": "Reindirizzamenti massimi",
|
||||||
"Accepted Status Codes": "Codici di stato accettati",
|
"Accepted Status Codes": "Codici di stato accettati",
|
||||||
Save: "Salva",
|
Save: "Salva",
|
||||||
Notifications: "Notifiche",
|
Notifications: "Notifiche",
|
||||||
|
@ -110,38 +118,64 @@ export default {
|
||||||
"Last Result": "Ultimo risultato",
|
"Last Result": "Ultimo risultato",
|
||||||
"Create your admin account": "Crea l'account amministratore",
|
"Create your admin account": "Crea l'account amministratore",
|
||||||
"Repeat Password": "Ripeti Password",
|
"Repeat Password": "Ripeti Password",
|
||||||
|
"Import Backup": "Importa Backup",
|
||||||
|
"Export Backup": "Esporta Backup",
|
||||||
|
Export: "Esporta",
|
||||||
|
Import: "Importa",
|
||||||
respTime: "Tempo di Risposta (ms)",
|
respTime: "Tempo di Risposta (ms)",
|
||||||
notAvailableShort: "N/D",
|
notAvailableShort: "N/D",
|
||||||
|
"Default enabled": "Abilitato di default",
|
||||||
|
"Apply on all existing monitors": "Applica su tutti i monitoraggi",
|
||||||
Create: "Crea",
|
Create: "Crea",
|
||||||
"Clear Data": "Cancella dati",
|
"Clear Data": "Cancella dati",
|
||||||
Events: "Eventi",
|
Events: "Eventi",
|
||||||
Heartbeats: "Controlli",
|
Heartbeats: "Controlli",
|
||||||
"Auto Get": "Auto Get",
|
"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.",
|
backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi.",
|
||||||
Export: "Export",
|
backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.",
|
||||||
Import: "Import",
|
alertNoFile: "Selezionare il file da importare.",
|
||||||
"Default enabled": "Default enabled",
|
alertWrongFileType: "Selezionare un file JSON.",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Clear all statistics": "Pulisci tutte le statistiche",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
"Skip existing": "Ignora gli esistenti",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
Overwrite: "Sovrascrivi",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
Options: "Opzioni",
|
||||||
alertNoFile: "Please select a file to import.",
|
"Keep both": "Mantieni entrambi",
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
"Verify Token": "Verifica Token",
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
"Setup 2FA": "Imposta l'autenticazione a due fattori",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
"Enable 2FA": "Abilita l'autenticazione a due fattori",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
"Disable 2FA": "Disabilita l'autenticazione a due fattori",
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
"2FA Settings": "Impostazioni autenticazione a due fattori",
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
"Two Factor Authentication": "Autenticazione a due fattori",
|
||||||
"Verify Token": "Verify Token",
|
Active: "Attivata",
|
||||||
"Setup 2FA": "Setup 2FA",
|
Inactive: "Disattivata",
|
||||||
"Enable 2FA": "Enable 2FA",
|
|
||||||
"Disable 2FA": "Disable 2FA",
|
|
||||||
"2FA Settings": "2FA Settings",
|
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
|
||||||
Active: "Active",
|
|
||||||
Inactive: "Inactive",
|
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Mostra URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
Tags: "Etichette",
|
||||||
}
|
"Add New below or Select...": "Aggiungine una oppure scegli...",
|
||||||
|
"Tag with this name already exist.": "Un'etichetta con questo nome già esiste.",
|
||||||
|
"Tag with this value already exist.": "Un'etichetta con questo valore già esiste.",
|
||||||
|
color: "colori",
|
||||||
|
"value (optional)": "valore (opzionale)",
|
||||||
|
Gray: "Grigio",
|
||||||
|
Red: "Rosso",
|
||||||
|
Orange: "Arancione",
|
||||||
|
Green: "Verde",
|
||||||
|
Blue: "Blu",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Viola",
|
||||||
|
Pink: "Rosa",
|
||||||
|
"Search...": "Cerca...",
|
||||||
|
"Avg. Ping": "Ping medio",
|
||||||
|
"Avg. Response": "Risposta media",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Non c'è nulla qui, aggiungere un gruppo oppure un monitoraggio.",
|
||||||
|
"No Services": "Nessun Servizio",
|
||||||
|
"All Systems Operational": "Tutti i sistemi sono operativi",
|
||||||
|
"Partially Degraded Service": "Servizio parzialmente degradato",
|
||||||
|
"Degraded Service": "Servizio degradato",
|
||||||
|
"Add Group": "Aggiungi Gruppo",
|
||||||
|
"Add a monitor": "Aggiungi un monitoraggio",
|
||||||
|
"Edit Status Page": "Modifica pagina di stato",
|
||||||
|
"Go to Dashboard": "Vai al Cruscotto",
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "日本語",
|
languageName: "日本語",
|
||||||
checkEverySecond: "{0}秒ごとにチェックします。",
|
checkEverySecond: "{0}秒ごとにチェックします。",
|
||||||
"Avg.": "平均",
|
|
||||||
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
|
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
|
||||||
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
|
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
|
||||||
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
|
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
|
||||||
|
@ -120,7 +119,6 @@ export default {
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
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",
|
"Default enabled": "Default enabled",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
|
||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||||
|
@ -144,4 +142,41 @@ export default {
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Clear all Statistics",
|
||||||
}
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
|
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||||
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
|
"Skip existing": "Skip existing",
|
||||||
|
Overwrite: "Overwrite",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Keep both",
|
||||||
|
Tags: "Tags",
|
||||||
|
"Add New below or Select...": "Add New below or Select...",
|
||||||
|
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||||
|
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||||
|
color: "color",
|
||||||
|
"value (optional)": "value (optional)",
|
||||||
|
Gray: "Gray",
|
||||||
|
Red: "Red",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Green",
|
||||||
|
Blue: "Blue",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Purple",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Avg. Ping": "Avg. Ping",
|
||||||
|
"Avg. Response": "Avg. Response",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "한국어",
|
languageName: "한국어",
|
||||||
checkEverySecond: "{0} 초마다 체크해요.",
|
checkEverySecond: "{0} 초마다 체크해요.",
|
||||||
"Avg.": "평균",
|
|
||||||
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
|
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
|
||||||
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
|
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
|
||||||
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
|
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
|
||||||
|
@ -120,7 +119,6 @@ export default {
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
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",
|
"Default enabled": "Default enabled",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
|
||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||||
|
@ -144,4 +142,41 @@ export default {
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Clear all Statistics",
|
||||||
}
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
|
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||||
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
|
"Skip existing": "Skip existing",
|
||||||
|
Overwrite: "Overwrite",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Keep both",
|
||||||
|
Tags: "Tags",
|
||||||
|
"Add New below or Select...": "Add New below or Select...",
|
||||||
|
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||||
|
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||||
|
color: "color",
|
||||||
|
"value (optional)": "value (optional)",
|
||||||
|
Gray: "Gray",
|
||||||
|
Red: "Red",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Green",
|
||||||
|
Blue: "Blue",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Purple",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Avg. Ping": "Avg. Ping",
|
||||||
|
"Avg. Response": "Avg. Response",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
languageName: "Nederlands",
|
languageName: "Nederlands",
|
||||||
checkEverySecond: "Controleer elke {0} seconden.",
|
checkEverySecond: "Controleer elke {0} seconden.",
|
||||||
"Avg.": "Gem.",
|
|
||||||
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
|
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
|
||||||
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
|
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
|
||||||
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
|
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
|
||||||
|
@ -16,6 +15,14 @@ export default {
|
||||||
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
|
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
|
||||||
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
|
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
|
||||||
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
|
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
|
||||||
|
enableDefaultNotificationDescription: "Voor elke nieuwe monitor wordt deze melding standaard ingeschakeld. U kunt de melding nog steeds afzonderlijk uitschakelen voor elke monitor.",
|
||||||
|
clearEventsMsg: "Weet je zeker dat je alle evenementen voor deze monitor wilt verwijderen?",
|
||||||
|
clearHeartbeatsMsg: "Weet je zeker dat je alle heartbeats voor deze monitor wilt verwijderen?",
|
||||||
|
confirmClearStatisticsMsg: "Weet u zeker dat u alle statistieken wilt verwijderen?",
|
||||||
|
twoFAVerifyLabel: "Voer uw 2FA controle token in voor verificatie",
|
||||||
|
tokenValidSettingsMsg: "Token is geldig! U kunt nu de 2FA-instellingen opslaan.",
|
||||||
|
confirmEnableTwoFAMsg: "Weet je zeker dat je 2FA wilt inschakelen?",
|
||||||
|
confirmDisableTwoFAMsg: "Weet je zeker dat je 2FA wilt uitschakelen?",
|
||||||
Settings: "Instellingen",
|
Settings: "Instellingen",
|
||||||
Dashboard: "Dashboard",
|
Dashboard: "Dashboard",
|
||||||
"New Update": "Nieuwe update",
|
"New Update": "Nieuwe update",
|
||||||
|
@ -80,7 +87,7 @@ export default {
|
||||||
"Allow indexing": "Indexering toestaan",
|
"Allow indexing": "Indexering toestaan",
|
||||||
"Discourage search engines from indexing site": "Ontmoedig zoekmachines om de site te indexeren",
|
"Discourage search engines from indexing site": "Ontmoedig zoekmachines om de site te indexeren",
|
||||||
"Change Password": "Verander wachtwoord",
|
"Change Password": "Verander wachtwoord",
|
||||||
"Current Password": "Huidig wachtwoord",
|
"Current Password": "Huidig wachtwoord",
|
||||||
"New Password": "Nieuw wachtwoord",
|
"New Password": "Nieuw wachtwoord",
|
||||||
"Repeat New Password": "Herhaal nieuw wachtwoord",
|
"Repeat New Password": "Herhaal nieuw wachtwoord",
|
||||||
"Update Password": "Vernieuw wachtwoord",
|
"Update Password": "Vernieuw wachtwoord",
|
||||||
|
@ -107,41 +114,69 @@ export default {
|
||||||
"Last Result": "Laatste resultaat",
|
"Last Result": "Laatste resultaat",
|
||||||
"Create your admin account": "Maak uw beheerdersaccount aan",
|
"Create your admin account": "Maak uw beheerdersaccount aan",
|
||||||
"Repeat Password": "Herhaal wachtwoord",
|
"Repeat Password": "Herhaal wachtwoord",
|
||||||
|
Export: "Exporteren",
|
||||||
|
Import: "Importeren",
|
||||||
respTime: "resp. tijd (ms)",
|
respTime: "resp. tijd (ms)",
|
||||||
notAvailableShort: "N.v.t.",
|
notAvailableShort: "N.v.t.",
|
||||||
Create: "Create",
|
"Default enabled": "Default enabled",
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
"Apply on all existing monitors": "Pas toe op alle bestaande monitors",
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
Create: "Aanmaken",
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
"Clear Data": "Data wissen",
|
||||||
"Clear Data": "Clear Data",
|
Events: "Gebeurtenissen",
|
||||||
Events: "Events",
|
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Heartbeats",
|
||||||
"Auto Get": "Auto Get",
|
"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.",
|
backupDescription: "U kunt een back-up maken van alle monitoren en alle meldingen in een JSON-bestand.",
|
||||||
"Default enabled": "Default enabled",
|
backupDescription2: "PS: Geschiedenis- en gebeurtenisgegevens zijn niet inbegrepen.",
|
||||||
|
backupDescription3: "Gevoelige gegevens zoals melding tokens zijn opgenomen in het exportbestand, houd het veilig opgeslagen.",
|
||||||
|
alertNoFile: "Selecteer een bestand om te importeren.",
|
||||||
|
alertWrongFileType: "Selecteer een JSON-bestand.",
|
||||||
|
"Verify Token": "Controleer token",
|
||||||
|
"Setup 2FA": "2FA instellingen",
|
||||||
|
"Enable 2FA": "Schakel 2FA in",
|
||||||
|
"Disable 2FA": "Schakel 2FA uit",
|
||||||
|
"2FA Settings": "2FA-instellingen",
|
||||||
|
"Two Factor Authentication": "Two Factor Authenticatie",
|
||||||
|
Active: "Actief",
|
||||||
|
Inactive: "Inactief",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Also apply to existing monitors",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
|
||||||
Export: "Export",
|
|
||||||
Import: "Import",
|
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
|
||||||
alertNoFile: "Please select a file to import.",
|
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
|
||||||
"Verify Token": "Verify Token",
|
|
||||||
"Setup 2FA": "Setup 2FA",
|
|
||||||
"Enable 2FA": "Enable 2FA",
|
|
||||||
"Disable 2FA": "Disable 2FA",
|
|
||||||
"2FA Settings": "2FA Settings",
|
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
|
||||||
Active: "Active",
|
|
||||||
Inactive: "Inactive",
|
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Toon URI",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Wis alle statistieken",
|
||||||
}
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
|
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||||
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
|
"Import Backup": "Import Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
|
"Skip existing": "Skip existing",
|
||||||
|
Overwrite: "Overwrite",
|
||||||
|
Options: "Options",
|
||||||
|
"Keep both": "Keep both",
|
||||||
|
Tags: "Tags",
|
||||||
|
"Add New below or Select...": "Add New below or Select...",
|
||||||
|
"Tag with this name already exist.": "Tag with this name already exist.",
|
||||||
|
"Tag with this value already exist.": "Tag with this value already exist.",
|
||||||
|
color: "color",
|
||||||
|
"value (optional)": "value (optional)",
|
||||||
|
Gray: "Gray",
|
||||||
|
Red: "Red",
|
||||||
|
Orange: "Orange",
|
||||||
|
Green: "Green",
|
||||||
|
Blue: "Blue",
|
||||||
|
Indigo: "Indigo",
|
||||||
|
Purple: "Purple",
|
||||||
|
Pink: "Pink",
|
||||||
|
"Search...": "Search...",
|
||||||
|
"Avg. Ping": "Avg. Ping",
|
||||||
|
"Avg. Response": "Avg. Response",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue