Merge branch 'master' into Revyn112_master

# Conflicts:
#	server/model/monitor.js
#	src/languages/en.js
#	src/pages/EditMonitor.vue
This commit is contained in:
Louis Lam 2021-10-18 15:50:35 +08:00
commit ae31eb6ba9
154 changed files with 16963 additions and 4471 deletions

View file

@ -19,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
@ -28,7 +27,8 @@ CNAME
install.sh install.sh
SECURITY.md SECURITY.md
tsconfig.json tsconfig.json
.env
/tmp
### .gitignore content (commented rules are duplicated) ### .gitignore content (commented rules are duplicated)

View file

@ -91,6 +91,23 @@ module.exports = {
"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,
},
} }
] ]
}; };

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username #patreon: # Replace with a single Patreon username
open_collective: uptime-kuma # Replace with a single Open Collective username open_collective: uptime-kuma # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username #ko_fi: # Replace with a single Ko-fi username

View file

@ -9,6 +9,9 @@ assignees: ''
**Is it a duplicate question?** **Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Describe your problem**
Please describe what you are asking for
**Info** **Info**
Uptime Kuma Version: Uptime Kuma Version:
Using Docker?: Yes/No Using Docker?: Yes/No

35
.github/workflows/auto-test.yml vendored Normal file
View file

@ -0,0 +1,35 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Auto Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
auto-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
node-version: [14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm run install-legacy
- run: npm run build
- run: npm test
env:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}

2
.gitignore vendored
View file

@ -11,3 +11,5 @@ dist-ssr
/private /private
/out /out
/tmp
.env

View file

@ -4,54 +4,73 @@ First of all, thank you everyone who made pull requests for Uptime Kuma, I never
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json. The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working. The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
# Can I create a pull request for Uptime Kuma? ## Key Technical Skills
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested. - Node.js (You should know what are promise, async/await and arrow function etc.)
- Socket.io
- SCSS
- Vue.js
- Bootstrap
- SQLite
## Directories
- data (App data)
- dist (Frontend build)
- extra (Extra useful scripts)
- public (Frontend resources for dev only)
- server (Server source code)
- src (Frontend source code)
- test (unit test)
## Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
If you are not sure, feel free to create an empty pull request draft first. If you are not sure, feel free to create an empty pull request draft first.
## Pull Request Examples ### Pull Request Examples
### ✅ High - Medium Priority #### ✅ High - Medium Priority
- Add a new notification - Add a new notification
- Add a chart - Add a chart
- Fix a bug - Fix a bug
- Translations
### *️⃣ 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.
- Change my release approach - Change my release approach
### ❌ Won't Merge #### ❌ Won't Merge
- Duplicated pull request - Duplicated pull request
- Buggy - Buggy
- Existing logic is completely modified or deleted - Existing logic is completely modified or deleted
- A function that is completely out of scope - A function that is completely out of scope
# Project Styles ## Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app. 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-compose 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
- 4 spaces indentation
- Follow `.editorconfig` - Follow `.editorconfig`
- Follow ESLint - Follow ESLint
@ -61,26 +80,20 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- SQLite: underscore_type - SQLite: underscore_type
- 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
```bash
npm install --legacy-peer-deps --dev
```
# Backend Dev
(2021-09-23 Update) (2021-09-23 Update)
@ -90,41 +103,39 @@ npm run start-server-dev
It binds to `0.0.0.0:3001` by default. It binds to `0.0.0.0:3001` by default.
## Backend Details ### 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` by default. ## How to start the Frontend Dev Server
1. Set the env var `NODE_ENV` to "development".
2. Start the frontend dev server by the following command.
```bash ```bash
npm run dev npm run dev
``` ```
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix. It binds to `0.0.0.0:3000` by default.
You can use Vue.js devtools Chrome extension for debugging. You can use Vue.js devtools Chrome extension for debugging.
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh: ### Build the frontend
```javascript
localStorage.dev = "dev";
```
So that the frontend will try to connect websocket server in 3001.
Alternately, you can specific `NODE_ENV` to "development".
## Build the frontend
```bash ```bash
npm run build npm run build
``` ```
## Frontend Details ### Frontend Details
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.
@ -134,11 +145,36 @@ As you can see, most data in frontend is stored in root level, even though you c
The data and socket logic are in `src/mixins/socket.js`. 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/`
2. 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.
```bash
npm run build
npm test
```
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
## Update Dependencies
Install `ncu`
https://github.com/raineorshine/npm-check-updates
```bash
ncu -u -t patch
npm install
```
Since previously updating vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
## Translations
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages

View file

@ -1,6 +1,6 @@
# 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="" />
@ -8,7 +8,7 @@
It is a self-hosted monitoring tool like "Uptime Robot". It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" /> <img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
## 🥔 Live Demo ## 🥔 Live Demo
@ -16,17 +16,20 @@ Try it!
https://demo.uptime.kuma.pet https://demo.uptime.kuma.pet
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it. It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much! 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 / Push.
* 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/tree/master/src/components/notifications).
* 20 seconds interval. * 20 second intervals.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages) * [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
* Simple Status Page
* Ping Chart
* Certificate Info
## 🔧 How to Install ## 🔧 How to Install
@ -37,7 +40,7 @@ docker volume create uptime-kuma
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1 docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
``` ```
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after starting.
### 💪🏻 Without Docker ### 💪🏻 Without Docker
@ -55,11 +58,11 @@ npm run setup
node server/server.js node server/server.js
# (Recommended) Option 2. Run in background using PM2 # (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have: npm install pm2 -g # Install PM2 if you don't have it: npm install pm2 -g
pm2 start server/server.js --name uptime-kuma pm2 start server/server.js --name uptime-kuma
``` ```
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after starting.
### Advanced Installation ### Advanced Installation
@ -85,9 +88,13 @@ https://github.com/louislam/uptime-kuma/projects/1
## 🖼 More Screenshots ## 🖼 More Screenshots
Dark Mode: Light Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" /> <img src="https://uptime.kuma.pet/img/light.jpg" width="512" alt="" />
Status Page:
<img src="https://user-images.githubusercontent.com/1336778/134628766-a3fe0981-0926-4285-ab46-891a21c3e4cb.png" width="512" alt="" />
Settings Page: Settings Page:
@ -111,11 +118,13 @@ If you love this project, please consider giving me a ⭐.
## 🗣️ Discussion ## 🗣️ Discussion
### Issues Page ### Issues Page
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues). You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
### Subreddit ### Subreddit
My Reddit account: louislamlam My Reddit account: louislamlam
You can mention me if you ask question on Reddit. You can mention me if you ask a question on Reddit.
https://www.reddit.com/r/UptimeKuma/ https://www.reddit.com/r/UptimeKuma/
## Contribute ## Contribute

View file

@ -5,11 +5,27 @@
Use this section to tell people about which versions of your project are Use this section to tell people about which versions of your project are
currently being supported with security updates. currently being supported with security updates.
### Uptime Kuma Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 1.x.x | :white_check_mark: | | 1.8.X | :white_check_mark: |
| <= 1.7.X | ❌ |
### Upgradable Docker Tags
| Tag | Supported |
| ------- | ------------------ |
| 1 | :white_check_mark: |
| 1-debian | :white_check_mark: |
| 1-alpine | :white_check_mark: |
| latest | :white_check_mark: |
| debian | :white_check_mark: |
| alpine | :white_check_mark: |
| All other tags | ❌ |
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report security issues to uptime@kuma.pet. Please report security issues to uptime@kuma.pet.
Do not use the issue tracker or discuss it in the public as it will cause more damage. Do not use the issue tracker or discuss it in the public as it will cause more damage.

11
babel.config.js Normal file
View file

@ -0,0 +1,11 @@
const config = {};
if (process.env.TEST_FRONTEND) {
config.presets = ["@babel/preset-env"];
}
if (process.env.TEST_BACKEND) {
config.plugins = ["babel-plugin-rewire"];
}
module.exports = config;

View file

@ -0,0 +1,5 @@
module.exports = {
"rootDir": "..",
"testRegex": "./test/backend.spec.js",
};

View file

@ -0,0 +1,5 @@
module.exports = {
"rootDir": "..",
"testRegex": "./test/frontend.spec.js",
};

View file

@ -0,0 +1,6 @@
module.exports = {
"launch": {
"headless": process.env.HEADLESS_TEST || false,
"userDataDir": "./data/test-chrome-profile",
}
};

11
config/jest.config.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
"verbose": true,
"preset": "jest-puppeteer",
"globals": {
"__DEV__": true
},
"testRegex": "./test/e2e.spec.js",
"rootDir": "..",
"testTimeout": 30000,
};

24
config/vite.config.js Normal file
View file

@ -0,0 +1,24 @@
import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ["ie > 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
})
],
css: {
postcss: {
"parser": postCssScss,
"map": false,
"plugins": [postcssRTLCSS]
}
},
});

View file

@ -0,0 +1,13 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD method TEXT default 'GET' not null;
ALTER TABLE monitor
ADD body TEXT default null;
ALTER TABLE monitor
ADD headers TEXT default null;
COMMIT;

View 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;

View 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

View file

@ -0,0 +1,12 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim
WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine!
RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise && \
rm -rf /var/lib/apt/lists/*

51
docker/dockerfile Normal file
View file

@ -0,0 +1,51 @@
FROM louislam/uptime-kuma:base-debian AS build
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY . .
RUN npm ci && \
npm run build && \
npm ci --production && \
chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly
# Upload the artifact to Github
FROM louislam/uptime-kuma:base-debian AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file
ARG GITHUB_TOKEN
ARG TARGETARCH
ARG PLATFORM=debian
ARG VERSION
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
ARG DIST=dist.tar.gz
COPY --from=build /app /app
RUN chmod +x /app/extra/upload-github-release-asset.sh
# Full Build
# RUN tar -zcvf $FILE app
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE
# Dist only
RUN cd /app && tar -zcvf $DIST dist
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST

26
docker/dockerfile-alpine Normal file
View file

@ -0,0 +1,26 @@
FROM louislam/uptime-kuma:base-alpine AS build
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY . .
RUN npm ci && \
npm run build && \
npm ci --production && \
chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-alpine AS release
WORKDIR /app
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

View file

@ -1,33 +0,0 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim AS build
WORKDIR /app
COPY . .
RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-buster-slim AS release
WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
RUN apt update && \
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux && \
pip3 --no-cache-dir install apprise && \
rm -rf /var/lib/apt/lists/*
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

View file

@ -1,30 +0,0 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS build
WORKDIR /app
COPY . .
RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-alpine3.12 AS release
WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

6
ecosystem.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
apps: [{
name: "uptime-kuma",
script: "./server/server.js",
}]
}

57
extra/download-dist.js Normal file
View file

@ -0,0 +1,57 @@
console.log("Downloading dist");
const https = require("https");
const tar = require("tar");
const packageJSON = require("../package.json");
const fs = require("fs");
const version = packageJSON.version;
const filename = "dist.tar.gz";
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
download(url);
function download(url) {
console.log(url);
https.get(url, (response) => {
if (response.statusCode === 200) {
console.log("Extracting dist...");
if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) {
fs.rmdirSync("./dist-backup", {
recursive: true
});
}
fs.renameSync("./dist", "./dist-backup");
}
const tarStream = tar.x({
cwd: "./",
});
tarStream.on("close", () => {
fs.rmdirSync("./dist-backup", {
recursive: true
});
console.log("Done");
});
tarStream.on("error", () => {
if (fs.existsSync("./dist-backup")) {
fs.renameSync("./dist-backup", "./dist");
}
console.log("Done");
});
response.pipe(tarStream);
} else if (response.statusCode === 302) {
download(response.headers.location);
} else {
console.log("dist not found");
}
});
}

View file

@ -2,8 +2,8 @@
# set -e Exit the script if an error happens # set -e Exit the script if an error happens
set -e set -e
PUID=${PUID=1000} PUID=${PUID=0}
PGID=${PGID=1000} PGID=${PGID=0}
files_ownership () { 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. # -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.

View file

@ -12,13 +12,14 @@ const rl = readline.createInterface({
output: process.stdout output: process.stdout
}); });
(async () => { const main = async () => {
Database.init(args); Database.init(args);
await Database.connect(); await Database.connect();
try { try {
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
if (!process.env.TEST_BACKEND) {
const user = await R.findOne("user"); const user = await R.findOne("user");
if (! user) { if (! user) {
throw new Error("user not found, have you installed?"); throw new Error("user not found, have you installed?");
} }
@ -35,27 +36,35 @@ const rl = readline.createInterface({
// Reset all sessions by reset jwt secret // Reset all sessions by reset jwt secret
await initJWTSecret(); await initJWTSecret();
rl.close();
break; break;
} else { } else {
console.log("Passwords do not match, please try again."); console.log("Passwords do not match, please try again.");
} }
} }
console.log("Password reset successfully."); console.log("Password reset successfully.");
}
} catch (e) { } catch (e) {
console.error("Error: " + e.message); console.error("Error: " + e.message);
} }
await Database.close(); await Database.close();
rl.close();
console.log("Finished. You should restart the Uptime Kuma server.") console.log("Finished.");
})(); };
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {
resolve(answer); resolve(answer);
}) });
}); });
} }
if (!process.env.TEST_BACKEND) {
main();
}
module.exports = {
main,
};

View file

@ -26,10 +26,12 @@ const copyRecursiveSync = function (src, dest) {
} }
}; };
console.log("Arguments:", process.argv) console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en"; const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode); console.log("Base Lang: " + baseLangCode);
if (fs.existsSync("./languages")) {
fs.rmdirSync("./languages", { recursive: true }); fs.rmdirSync("./languages", { recursive: true });
}
copyRecursiveSync("../../src/languages", "./languages"); copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default; const en = (await import("./languages/en.js")).default;
@ -39,7 +41,7 @@ console.log("Files:", files);
for (const file of files) { for (const file of files) {
if (!file.endsWith(".js")) { if (!file.endsWith(".js")) {
console.log("Skipping " + file) console.log("Skipping " + file);
continue; continue;
} }

View file

@ -19,6 +19,7 @@ if (! newVersion) {
const exists = tagExists(newVersion); const exists = tagExists(newVersion);
if (! exists) { if (! exists) {
// Process package.json // Process package.json
pkg.version = newVersion; pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
@ -29,8 +30,11 @@ if (! exists) {
commit(newVersion); commit(newVersion);
tag(newVersion); tag(newVersion);
updateWiki(oldVersion, newVersion);
} else { } else {
console.log("version exists") console.log("version exists");
} }
function commit(version) { function commit(version) {
@ -38,16 +42,16 @@ function commit(version) {
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]); let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim(); let stdout = res.stdout.toString().trim();
console.log(stdout) console.log(stdout);
if (stdout.includes("no changes added to commit")) { if (stdout.includes("no changes added to commit")) {
throw new Error("commit error") throw new Error("commit error");
} }
} }
function tag(version) { function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]); let res = child_process.spawnSync("git", ["tag", version]);
console.log(res.stdout.toString().trim()) console.log(res.stdout.toString().trim());
} }
function tagExists(version) { function tagExists(version) {
@ -59,3 +63,38 @@ function tagExists(version) {
return res.stdout.toString().trim() === version; return res.stdout.toString().trim() === version;
} }
function updateWiki(oldVersion, newVersion) {
const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
safeDelete(wikiDir);
child_process.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]);
let content = fs.readFileSync(howToUpdateFilename).toString();
content = content.replaceAll(`git checkout ${oldVersion}`, `git checkout ${newVersion}`);
fs.writeFileSync(howToUpdateFilename, content);
child_process.spawnSync("git", ["add", "-A"], {
cwd: wikiDir,
});
child_process.spawnSync("git", ["commit", "-m", `Update to ${newVersion} from ${oldVersion}`], {
cwd: wikiDir,
});
console.log("Pushing to Github");
child_process.spawnSync("git", ["push"], {
cwd: wikiDir,
});
safeDelete(wikiDir);
}
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, {
recursive: true,
});
}
}

View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
#
# Author: Stefan Buck
# License: MIT
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
#
#
# This script accepts the following parameters:
#
# * owner
# * repo
# * tag
# * filename
# * github_api_token
#
# Script to upload a release asset using the GitHub API v3.
#
# Example:
#
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
#
# Check dependencies.
set -e
xargs=$(which gxargs || which xargs)
# Validate settings.
[ "$TRACE" ] && set -x
CONFIG=$@
for line in $CONFIG; do
eval "$line"
done
# Define variables.
GH_API="https://api.github.com"
GH_REPO="$GH_API/repos/$owner/$repo"
GH_TAGS="$GH_REPO/releases/tags/$tag"
AUTH="Authorization: token $github_api_token"
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
CURL_ARGS="-LJO#"
if [[ "$tag" == 'LATEST' ]]; then
GH_TAGS="$GH_REPO/releases/latest"
fi
# Validate token.
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
# Read asset tags.
response=$(curl -sH "$AUTH" $GH_TAGS)
# Get ID of the asset based on given filename.
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
# Upload asset
echo "Uploading asset... "
# Construct url
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET

12980
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.7.0", "version": "1.8.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,20 +15,29 @@
"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 --config ./config/vite.config.js",
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"build": "vite build", "build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
"test-with-build": "npm run build && npm test",
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js",
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
"tsc": "tsc", "tsc": "tsc",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.0-alpine --target release . --push", "build-docker-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.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.0-debian --target release . --push", "build-docker-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-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.8.0-alpine --target release . --push",
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.8.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.8.0-debian --target release . --push",
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-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 -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.7.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", "upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"update-version": "node extra/update-version.js", "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",
@ -43,66 +52,77 @@
"update-language-files": "cd extra/update-language-files && node index.js && 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",
"@louislam/sqlite3": "^5.0.6", "@louislam/sqlite3": "~6.0.0",
"@popperjs/core": "^2.10.1", "@popperjs/core": "~2.10.2",
"args-parser": "^1.3.0", "args-parser": "~1.3.0",
"axios": "^0.21.4", "axios": "~0.21.4",
"bcryptjs": "^2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "^5.1.1", "bootstrap": "~5.1.1",
"chart.js": "^3.5.1", "chardet": "^1.3.0",
"chartjs-adapter-dayjs": "^1.0.0", "bree": "~6.3.1",
"command-exists": "^1.2.9", "chart.js": "~3.5.1",
"compare-versions": "^3.6.0", "chartjs-adapter-dayjs": "~1.0.0",
"dayjs": "^1.10.7", "command-exists": "~1.2.9",
"express": "^4.17.1", "compare-versions": "~3.6.0",
"express-basic-auth": "^1.2.0", "dayjs": "~1.10.7",
"form-data": "^4.0.0", "express": "~4.17.1",
"http-graceful-shutdown": "^3.1.4", "express-basic-auth": "~1.2.0",
"jsonwebtoken": "^8.5.1", "form-data": "~4.0.0",
"nodemailer": "^6.6.5", "http-graceful-shutdown": "~3.1.4",
"notp": "^2.0.3", "iconv-lite": "^0.6.3",
"password-hash": "^1.2.2", "jsonwebtoken": "~8.5.1",
"prom-client": "^13.2.0", "nodemailer": "~6.6.5",
"prometheus-api-metrics": "^3.2.0", "notp": "~2.0.3",
"qrcode": "^1.4.4", "password-hash": "~1.2.2",
"postcss-rtlcss": "~3.4.1",
"postcss-scss": "~4.0.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.0",
"qrcode": "~1.4.4",
"redbean-node": "0.1.2", "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",
"tcp-ping": "^0.1.1", "tar": "^6.1.11",
"thirty-two": "^1.0.2", "tcp-ping": "~0.1.1",
"timezones-list": "^3.0.1", "thirty-two": "~1.0.2",
"v-pagination-3": "^0.1.6", "timezones-list": "~3.0.1",
"v-pagination-3": "~0.1.6",
"vue": "next", "vue": "next",
"vue-chart-3": "^0.5.8", "vue-chart-3": "~0.5.8",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "^3.0.4", "vue-contenteditable": "~3.0.4",
"vue-i18n": "^9.1.7", "vue-i18n": "~9.1.9",
"vue-image-crop-upload": "^3.0.3", "vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "^3.0.0-alpha.2", "vue-multiselect": "~3.0.0-alpha.2",
"vue-qrcode": "^1.0.0", "vue-qrcode": "~1.0.0",
"vue-router": "^4.0.11", "vue-router": "~4.0.11",
"vue-toastification": "^2.0.0-rc.1", "vue-toastification": "~2.0.0-rc.1",
"vuedraggable": "^4.1.0" "vuedraggable": "~4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.15.7", "@babel/eslint-parser": "~7.15.7",
"@types/bootstrap": "^5.1.6", "@babel/preset-env": "^7.15.8",
"@vitejs/plugin-legacy": "^1.5.3", "@types/bootstrap": "~5.1.6",
"@vitejs/plugin-vue": "^1.9.1", "@vitejs/plugin-legacy": "~1.6.1",
"@vue/compiler-sfc": "^3.2.16", "@vitejs/plugin-vue": "~1.9.2",
"core-js": "^3.18.0", "@vue/compiler-sfc": "~3.2.19",
"cross-env": "^7.0.3", "babel-plugin-rewire": "~1.2.0",
"dns2": "^2.0.1", "core-js": "~3.18.1",
"eslint": "^7.32.0", "cross-env": "~7.0.3",
"eslint-plugin-vue": "^7.18.0", "dns2": "~2.0.1",
"sass": "^1.42.1", "eslint": "~7.32.0",
"stylelint": "^13.13.1", "eslint-plugin-vue": "~7.18.0",
"stylelint-config-standard": "^22.0.0", "jest": "~27.2.4",
"typescript": "^4.4.3", "jest-puppeteer": "~6.0.0",
"vite": "^2.5.10" "puppeteer": "~10.4.0",
"sass": "~1.42.1",
"stylelint": "~13.13.1",
"stylelint-config-standard": "~22.0.0",
"typescript": "~4.4.3",
"vite": "~2.6.4"
} }
} }

View file

@ -1,6 +1,5 @@
const { setSetting } = require("./util-server"); const { setSetting } = require("./util-server");
const axios = require("axios"); const axios = require("axios");
const { isDev } = require("../src/util");
exports.version = require("../package.json").version; exports.version = require("../package.json").version;
exports.latestVersion = null; exports.latestVersion = null;
@ -22,7 +21,6 @@ exports.startInterval = () => {
} }
exports.latestVersion = res.data.version; exports.latestVersion = res.data.version;
console.log("Latest Version: " + exports.latestVersion);
} catch (_) { } } catch (_) { }
}; };

View file

@ -4,6 +4,8 @@
const { TimeLogger } = require("../src/util"); const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { io } = require("./server"); const { io } = require("./server");
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -14,10 +16,10 @@ async function sendNotificationList(socket) {
]); ]);
for (let bean of list) { for (let bean of list) {
result.push(bean.export()) result.push(bean.export());
} }
io.to(socket.userID).emit("notificationList", result) io.to(socket.userID).emit("notificationList", result);
timeLogger.print("Send Notification List"); timeLogger.print("Send Notification List");
@ -39,7 +41,7 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
LIMIT 100 LIMIT 100
`, [ `, [
monitorID, monitorID,
]) ]);
let result = list.reverse(); let result = list.reverse();
@ -69,7 +71,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
LIMIT 500 LIMIT 500
`, [ `, [
monitorID, monitorID,
]) ]);
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`); timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
@ -81,8 +83,18 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
} }
async function sendInfo(socket) {
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
primaryBaseURL: await setting("primaryBaseURL")
});
}
module.exports = { module.exports = {
sendNotificationList, sendNotificationList,
sendImportantHeartbeatList, sendImportantHeartbeatList,
sendHeartbeatList, sendHeartbeatList,
} sendInfo
};

7
server/config.js Normal file
View file

@ -0,0 +1,7 @@
const args = require("args-parser")(process.argv);
const demoMode = args["demo"] || false;
module.exports = {
args,
demoMode
};

View file

@ -49,6 +49,8 @@ class Database {
"patch-add-apikey-monitor.sql": true, "patch-add-apikey-monitor.sql": true,
"patch-incident-table.sql": true, "patch-incident-table.sql": true,
"patch-group-table.sql": true, "patch-group-table.sql": true,
"patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true,
} }
/** /**
@ -108,6 +110,7 @@ class Database {
R.freeze(true); 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");

31
server/jobs.js Normal file
View file

@ -0,0 +1,31 @@
const path = require("path");
const Bree = require("bree");
const { SHARE_ENV } = require("worker_threads");
const jobs = [
{
name: "clear-old-data",
interval: "at 03:14",
}
];
const initBackgroundJobs = function (args) {
const bree = new Bree({
root: path.resolve("server", "jobs"),
jobs,
worker: {
env: SHARE_ENV,
workerData: args,
},
workerMessageHandler: (message) => {
console.log("[Background Job]:", message);
}
});
bree.start();
return bree;
};
module.exports = {
initBackgroundJobs
};

View file

@ -0,0 +1,40 @@
const { log, exit, connectDb } = require("./util-worker");
const { R } = require("redbean-node");
const { setSetting, setting } = require("../util-server");
const DEFAULT_KEEP_PERIOD = 180;
(async () => {
await connectDb();
let period = await setting("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
// Try parse setting
let parsedPeriod;
try {
parsedPeriod = parseInt(period);
} catch (_) {
log("Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
log(`Clearing Data older than ${parsedPeriod} days...`);
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[parsedPeriod]
);
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
}
exit();
})();

View file

@ -0,0 +1,39 @@
const { parentPort, workerData } = require("worker_threads");
const Database = require("../database");
const path = require("path");
const log = function (any) {
if (parentPort) {
parentPort.postMessage(any);
}
};
const exit = function (error) {
if (error && error != 0) {
process.exit(error);
} else {
if (parentPort) {
parentPort.postMessage("done");
} else {
process.exit(0);
}
}
};
const connectDb = async function () {
const dbPath = path.join(
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
);
Database.init({
"data-dir": dbPath,
});
await Database.connect();
};
module.exports = {
log,
exit,
connectDb,
};

View file

@ -11,7 +11,9 @@ const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalCli
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 { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
/** /**
* status: * status:
@ -53,13 +55,15 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
url: this.url, url: this.url,
method: this.method,
body: this.body,
headers: this.headers,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
weight: this.weight, weight: this.weight,
active: this.active, active: this.active,
type: this.type, type: this.type,
apikey: this.apikey,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
keyword: this.keyword, keyword: this.keyword,
@ -70,6 +74,7 @@ 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, tags: tags,
}; };
@ -136,11 +141,15 @@ class Monitor extends BeanModel {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
let res = await axios.get(this.url, { const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
...(this.body ? { data: JSON.parse(this.body) } : {}),
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
...(this.headers ? JSON.parse(this.headers) : {}),
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
@ -150,7 +159,8 @@ class Monitor extends BeanModel {
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
}, },
}); };
let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@ -166,7 +176,13 @@ class Monitor extends BeanModel {
} }
} }
if (process.env.TIMELOGGER === "1") {
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
}
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
console.log(res.data);
}
if (this.type === "http") { if (this.type === "http") {
bean.status = UP; bean.status = UP;
@ -237,6 +253,25 @@ 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 if (this.type === "steam") { } else if (this.type === "steam") {
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
const filter = `addr\\${this.hostname}:${this.port}`; const filter = `addr\\${this.hostname}:${this.port}`;
@ -277,6 +312,9 @@ class Monitor extends BeanModel {
} else { } else {
throw new Error(bean.msg + ", but server is not found"); throw new Error(bean.msg + ", but server is not found");
} }
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
} }
if (this.isUpsideDown()) { if (this.isUpsideDown()) {
@ -304,61 +342,23 @@ class Monitor extends BeanModel {
} }
} }
// * ? -> ANY STATUS = important [isFirstBeat] let beatInterval = this.interval;
// UP -> PENDING = not important
// * UP -> DOWN = important let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeat.status === UP && bean.status === DOWN) ||
(previousBeat.status === DOWN && bean.status === UP) ||
(previousBeat.status === PENDING && bean.status === DOWN);
// Mark as important if status changed, ignore pending pings, // Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up // Don't notify if disrupted changes to up
if (isImportant) { if (isImportant) {
bean.important = true; bean.important = true;
await Monitor.sendNotification(isFirstBeat, this, bean);
// Send only if the first beat is DOWN
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
this.id,
]);
let text;
if (bean.status === UP) {
text = "✅ Up";
} else {
text = "🔴 Down";
}
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
}
} else { } else {
bean.important = false; bean.important = false;
} }
let beatInterval = this.interval;
if (bean.status === UP) { if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} 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) {
if (this.retryInterval !== this.interval) { if (this.retryInterval > 0) {
beatInterval = this.retryInterval; 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}`); console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
@ -375,12 +375,27 @@ class Monitor extends BeanModel {
previousBeat = bean; previousBeat = bean;
if (! this.isStop) { if (! this.isStop) {
if (demoMode) {
if (beatInterval < 20) {
console.log("beat interval too low, reset to 20s");
beatInterval = 20;
}
}
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000); this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
} }
}; };
// Delay Push Type
if (this.type === "push") {
setTimeout(() => {
beat(); beat();
}, this.interval * 1000);
} else {
beat();
}
} }
stop() { stop() {
@ -541,6 +556,54 @@ class Monitor extends BeanModel {
const uptime = await this.calcUptime(duration, monitorID); const uptime = await this.calcUptime(duration, monitorID);
io.to(userID).emit("uptime", monitorID, duration, uptime); io.to(userID).emit("uptime", monitorID, duration, uptime);
} }
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
return isImportant;
}
static async sendNotification(isFirstBeat, monitor, bean) {
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
monitor.id,
]);
let text;
if (bean.status === UP) {
text = "✅ Up";
} else {
text = "🔴 Down";
}
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
// Clear Status Page Cache
apicache.clear();
}
}
} }
module.exports = Monitor; module.exports = Monitor;

View file

@ -0,0 +1,108 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
const Crypto = require("crypto");
const qs = require("qs");
class AliyunSMS extends NotificationProvider {
name = "AliyunSMS";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON != null) {
let msgBody = JSON.stringify({
name: monitorJSON["name"],
time: heartbeatJSON["time"],
status: this.statusToString(heartbeatJSON["status"]),
msg: heartbeatJSON["msg"],
});
if (this.sendSms(notification, msgBody)) {
return okMsg;
}
} else {
let msgBody = JSON.stringify({
name: "",
time: "",
status: "",
msg: msg,
});
if (this.sendSms(notification, msgBody)) {
return okMsg;
}
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
async sendSms(notification, msgbody) {
let params = {
PhoneNumbers: notification.phonenumber,
TemplateCode: notification.templateCode,
SignName: notification.signName,
TemplateParam: msgbody,
AccessKeyId: notification.accessKeyId,
Format: "JSON",
SignatureMethod: "HMAC-SHA1",
SignatureVersion: "1.0",
SignatureNonce: Math.random().toString(),
Timestamp: new Date().toISOString(),
Action: "SendSms",
Version: "2017-05-25",
};
params.Signature = this.sign(params, notification.secretAccessKey);
let config = {
method: "POST",
url: "http://dysmsapi.aliyuncs.com/",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: qs.stringify(params),
};
let result = await axios(config);
if (result.data.Message == "OK") {
return true;
}
return false;
}
/** Aliyun request sign */
sign(param, AccessKeySecret) {
let param2 = {};
let data = [];
let oa = Object.keys(param).sort();
for (let i = 0; i < oa.length; i++) {
let key = oa[i];
param2[key] = param[key];
}
for (let key in param2) {
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
}
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
return Crypto
.createHmac("sha1", `${AccessKeySecret}&`)
.update(Buffer.from(StringToSign))
.digest("base64");
}
statusToString(status) {
switch (status) {
case DOWN:
return "DOWN";
case UP:
return "UP";
default:
return status;
}
}
}
module.exports = AliyunSMS;

View file

@ -0,0 +1,79 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
const Crypto = require("crypto");
class DingDing extends NotificationProvider {
name = "DingDing";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON != null) {
let params = {
msgtype: "markdown",
markdown: {
title: monitorJSON["name"],
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
}
};
if (this.sendToDingDing(notification, params)) {
return okMsg;
}
} else {
let params = {
msgtype: "text",
text: {
content: msg
}
};
if (this.sendToDingDing(notification, params)) {
return okMsg;
}
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
async sendToDingDing(notification, params) {
let timestamp = Date.now();
let config = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
url: `${notification.webHookUrl}&timestamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
data: JSON.stringify(params),
};
let result = await axios(config);
if (result.data.errmsg == "ok") {
return true;
}
return false;
}
/** DingDing sign */
sign(timestamp, secretKey) {
return Crypto
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
.digest("base64");
}
statusToString(status) {
switch (status) {
case DOWN:
return "DOWN";
case UP:
return "UP";
default:
return status;
}
}
}
module.exports = DingDing;

View file

@ -0,0 +1,83 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Feishu extends NotificationProvider {
name = "Feishu";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let feishuWebHookUrl = notification.feishuWebHookUrl;
try {
if (heartbeatJSON == null) {
let testdata = {
msg_type: "text",
content: {
text: msg,
},
};
await axios.post(feishuWebHookUrl, testdata);
return okMsg;
}
if (heartbeatJSON["status"] == DOWN) {
let downdata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Down] " +
heartbeatJSON["msg"] +
"\nTime (UTC): " +
heartbeatJSON["time"],
},
],
],
},
},
},
};
await axios.post(feishuWebHookUrl, downdata);
return okMsg;
}
if (heartbeatJSON["status"] == UP) {
let updata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Up] " +
heartbeatJSON["msg"] +
"\nTime (UTC): " +
heartbeatJSON["time"],
},
],
],
},
},
},
};
await axios.post(feishuWebHookUrl, updata);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Feishu;

View file

@ -0,0 +1,45 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const Crypto = require("crypto");
const { debug } = require("../../src/util");
class Matrix extends NotificationProvider {
name = "matrix";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
const size = 20;
const randomString = encodeURIComponent(
Crypto
.randomBytes(size)
.toString("base64")
.slice(0, size)
);
debug("Random String: " + randomString);
const roomId = encodeURIComponent(notification.internalRoomId);
debug("Matrix Room ID: " + roomId);
try {
let config = {
headers: {
"Authorization": `Bearer ${notification.accessToken}`,
}
};
let data = {
"msgtype": "m.text",
"body": msg
};
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Matrix;

View file

@ -9,6 +9,8 @@ class Octopush extends NotificationProvider {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
try { try {
// Default - V2
if (notification.octopushVersion == 2 || !notification.octopushVersion) {
let config = { let config = {
headers: { headers: {
"api-key": notification.octopushAPIKey, "api-key": notification.octopushAPIKey,
@ -28,8 +30,30 @@ class Octopush extends NotificationProvider {
"purpose": "alert", "purpose": "alert",
"sender": notification.octopushSenderName "sender": notification.octopushSenderName
}; };
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config) await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
} else if (notification.octopushVersion == 1) {
let data = {
"user_login": notification.octopushDMLogin,
"api_key": notification.octopushDMAPIKey,
"sms_recipients": notification.octopushDMPhoneNumber,
"sms_sender": notification.octopushDMSenderName,
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX",
"transactional": "1",
//octopush not supporting non ascii char
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""),
};
let config = {
headers: {
"cache-control": "no-cache"
},
params: data
};
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config)
} else {
throw new Error("Unknown Octopush version!");
}
return okMsg; return okMsg;
} catch (error) { } catch (error) {
this.throwGeneralAxiosError(error); this.throwGeneralAxiosError(error);

View file

@ -0,0 +1,41 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class PromoSMS extends NotificationProvider {
name = "promosms";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'),
"Accept": "text/json",
}
};
let data = {
"recipients": [ notification.promosmsPhoneNumber ],
//Lets remove non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": Number(notification.promosmsSMSType),
"sender": notification.promosmsSenderName
};
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config);
if (resp.data.response.status !== 0) {
let error = "Something gone wrong. Api returned " + resp.data.response.status + ".";
this.throwGeneralAxiosError(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = PromoSMS;

View file

@ -1,5 +1,8 @@
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const axios = require("axios"); const axios = require("axios");
const Slack = require("./slack");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
class RocketChat extends NotificationProvider { class RocketChat extends NotificationProvider {
@ -10,16 +13,17 @@ class RocketChat extends NotificationProvider {
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let data = {
"text": "Uptime Kuma Rocket.chat testing successful.", "text": msg,
"channel": notification.rocketchannel, "channel": notification.rocketchannel,
"username": notification.rocketusername, "username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo, "icon_emoji": notification.rocketiconemo,
} };
await axios.post(notification.rocketwebhookURL, data) await axios.post(notification.rocketwebhookURL, data);
return okMsg; return okMsg;
} }
const time = heartbeatJSON["time"]; const time = heartbeatJSON["time"];
let data = { let data = {
"text": "Uptime Kuma Alert", "text": "Uptime Kuma Alert",
"channel": notification.rocketchannel, "channel": notification.rocketchannel,
@ -28,16 +32,32 @@ class RocketChat extends NotificationProvider {
"attachments": [ "attachments": [
{ {
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time, "title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
"title_link": notification.rocketbutton,
"text": "*Message*\n" + msg, "text": "*Message*\n" + msg,
"color": "#32cd32"
} }
] ]
};
// Color
if (heartbeatJSON.status === DOWN) {
data.attachments[0].color = "#ff0000";
} else {
data.attachments[0].color = "#32cd32";
} }
await axios.post(notification.rocketwebhookURL, data)
if (notification.rocketbutton) {
await Slack.deprecateURL(notification.rocketbutton);
}
const baseURL = await setting("primaryBaseURL");
if (baseURL) {
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
}
await axios.post(notification.rocketwebhookURL, data);
return okMsg; return okMsg;
} catch (error) { } catch (error) {
this.throwGeneralAxiosError(error) this.throwGeneralAxiosError(error);
} }
} }

View file

@ -1,27 +1,47 @@
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const axios = require("axios"); const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
class Slack extends NotificationProvider { class Slack extends NotificationProvider {
name = "slack"; name = "slack";
/**
* Deprecated property notification.slackbutton
* Set it as primary base url if this is not yet set.
*/
static async deprecateURL(url) {
let currentPrimaryBaseURL = await setting("primaryBaseURL");
if (!currentPrimaryBaseURL) {
console.log("Move the url to be the primary base URL");
await setSettings("general", {
primaryBaseURL: url,
});
} else {
console.log("Already there, no need to move the primary base URL");
}
}
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let data = {
"text": "Uptime Kuma Slack testing successful.", "text": msg,
"channel": notification.slackchannel, "channel": notification.slackchannel,
"username": notification.slackusername, "username": notification.slackusername,
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,
} };
await axios.post(notification.slackwebhookURL, data) await axios.post(notification.slackwebhookURL, data);
return okMsg; return okMsg;
} }
const time = heartbeatJSON["time"]; const time = heartbeatJSON["time"];
const textMsg = "Uptime Kuma Alert";
let data = { let data = {
"text": "Uptime Kuma Alert", "text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
"channel": notification.slackchannel, "channel": notification.slackchannel,
"username": notification.slackusername, "username": notification.slackusername,
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,
@ -42,26 +62,35 @@ class Slack extends NotificationProvider {
"type": "mrkdwn", "type": "mrkdwn",
"text": "*Time (UTC)*\n" + time, "text": "*Time (UTC)*\n" + time,
}], }],
}, }],
{ };
if (notification.slackbutton) {
await Slack.deprecateURL(notification.slackbutton);
}
const baseURL = await setting("primaryBaseURL");
// Button
if (baseURL) {
data.blocks.push({
"type": "actions", "type": "actions",
"elements": [ "elements": [{
{
"type": "button", "type": "button",
"text": { "text": {
"type": "plain_text", "type": "plain_text",
"text": "Visit Uptime Kuma", "text": "Visit Uptime Kuma",
}, },
"value": "Uptime-Kuma", "value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma", "url": baseURL + getMonitorRelativeURL(monitorJSON.id),
},
],
}], }],
});
} }
await axios.post(notification.slackwebhookURL, data)
await axios.post(notification.slackwebhookURL, data);
return okMsg; return okMsg;
} catch (error) { } catch (error) {
this.throwGeneralAxiosError(error) this.throwGeneralAxiosError(error);
} }
} }

View file

@ -1,5 +1,6 @@
const nodemailer = require("nodemailer"); const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
class SMTP extends NotificationProvider { class SMTP extends NotificationProvider {
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
pass: notification.smtpPassword, pass: notification.smtpPassword,
}; };
} }
// Lets start with default subject and empty string for custom one
let subject = msg;
// Change the subject if:
// - The msg ends with "Testing" or
// - Actual Up/Down Notification
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
let customSubject = "";
// Our subject cannot end with whitespace it's often raise spam score
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
if (notification.customSubject) {
customSubject = notification.customSubject.trim();
}
// If custom subject is not empty, change subject for notification
if (customSubject !== "") {
// Replace "MACROS" with corresponding variable
let replaceName = new RegExp("{{NAME}}", "g");
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
let replaceStatus = new RegExp("{{STATUS}}", "g");
// Lets start with dummy values to simplify code
let monitorName = "Test";
let monitorHostnameOrURL = "testing.hostname";
let serviceStatus = "⚠️ Test";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
// Break replace to one by line for better readability
customSubject = customSubject.replace(replaceStatus, serviceStatus);
customSubject = customSubject.replace(replaceName, monitorName);
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
subject = customSubject;
}
}
let transporter = nodemailer.createTransport(config); let transporter = nodemailer.createTransport(config);
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
cc: notification.smtpCC, cc: notification.smtpCC,
bcc: notification.smtpBCC, bcc: notification.smtpBCC,
to: notification.smtpTo, to: notification.smtpTo,
subject: msg, subject: subject,
text: bodyTextContent, text: bodyTextContent,
tls: { tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false, rejectUnauthorized: notification.smtpIgnoreTLSError || false,

View file

@ -5,7 +5,9 @@ const Gotify = require("./notification-providers/gotify");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost"); const Mattermost = require("./notification-providers/mattermost");
const Matrix = require("./notification-providers/matrix");
const Octopush = require("./notification-providers/octopush"); const Octopush = require("./notification-providers/octopush");
const PromoSMS = require("./notification-providers/promosms");
const Pushbullet = require("./notification-providers/pushbullet"); const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover"); const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy"); const Pushy = require("./notification-providers/pushy");
@ -16,6 +18,9 @@ const SMTP = require("./notification-providers/smtp");
const Teams = require("./notification-providers/teams"); 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");
const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");
class Notification { class Notification {
@ -28,13 +33,18 @@ class Notification {
const list = [ const list = [
new Apprise(), new Apprise(),
new AliyunSms(),
new DingDing(),
new Discord(), new Discord(),
new Teams(), new Teams(),
new Gotify(), new Gotify(),
new Line(), new Line(),
new LunaSea(), new LunaSea(),
new Feishu(),
new Mattermost(), new Mattermost(),
new Matrix(),
new Octopush(), new Octopush(),
new PromoSMS(),
new Pushbullet(), new Pushbullet(),
new Pushover(), new Pushover(),
new Pushy(), new Pushy(),

View file

@ -4,10 +4,7 @@ const net = require("net");
const spawn = require("child_process").spawn; const spawn = require("child_process").spawn;
const events = require("events"); const events = require("events");
const fs = require("fs"); const fs = require("fs");
const WIN = /^win/.test(process.platform); const util = require("./util-server");
const LIN = /^linux/.test(process.platform);
const MAC = /^darwin/.test(process.platform);
const FBSD = /^freebsd/.test(process.platform);
module.exports = Ping; module.exports = Ping;
@ -23,12 +20,12 @@ function Ping(host, options) {
const timeout = 10; const timeout = 10;
if (WIN) { if (util.WIN) {
this._bin = "c:/windows/system32/ping.exe"; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} else if (LIN) { } else if (util.LIN) {
this._bin = "/bin/ping"; this._bin = "/bin/ping";
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ]; const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
@ -40,7 +37,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : defaultArgs; this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else if (MAC) { } else if (util.MAC) {
if (net.isIPv6(host) || options.ipv6) { if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6"; this._bin = "/sbin/ping6";
@ -51,7 +48,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ]; this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else if (FBSD) { } else if (util.FBSD) {
this._bin = "/sbin/ping"; this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ]; const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
}); });
this._ping.stdout.on("data", function (data) { // log stdout this._ping.stdout.on("data", function (data) { // log stdout
if (util.WIN) {
data = convertOutput(data);
}
this._stdout = (this._stdout || "") + data; this._stdout = (this._stdout || "") + data;
}); });
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
}); });
this._ping.stderr.on("data", function (data) { // log stderr this._ping.stderr.on("data", function (data) { // log stderr
if (util.WIN) {
data = convertOutput(data);
}
this._stderr = (this._stderr || "") + data; this._stderr = (this._stderr || "") + data;
}); });
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
Ping.prototype.stop = function () { Ping.prototype.stop = function () {
clearInterval(this._i); clearInterval(this._i);
}; };
/**
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
* Thank @pemassi
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
* @param data
* @returns {string}
*/
function convertOutput(data) {
if (util.WIN) {
if (data) {
return util.convertToUTF8(data);
}
}
return data;
}

View file

@ -6,7 +6,7 @@ const commonLabels = [
"monitor_url", "monitor_url",
"monitor_hostname", "monitor_hostname",
"monitor_port", "monitor_port",
] ];
const monitor_cert_days_remaining = new PrometheusClient.Gauge({ const monitor_cert_days_remaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining", name: "monitor_cert_days_remaining",
@ -41,45 +41,46 @@ class Prometheus {
monitor_url: monitor.url, monitor_url: monitor.url,
monitor_hostname: monitor.hostname, monitor_hostname: monitor.hostname,
monitor_port: monitor.port monitor_port: monitor.port
} };
} }
update(heartbeat, tlsInfo) { update(heartbeat, tlsInfo) {
if (typeof tlsInfo !== "undefined") { if (typeof tlsInfo !== "undefined") {
try { try {
let is_valid = 0 let is_valid = 0;
if (tlsInfo.valid == true) { if (tlsInfo.valid == true) {
is_valid = 1 is_valid = 1;
} else { } else {
is_valid = 0 is_valid = 0;
} }
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid) monitor_cert_is_valid.set(this.monitorLabelValues, is_valid);
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
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);
} }
} }
try { try {
monitor_status.set(this.monitorLabelValues, heartbeat.status) monitor_status.set(this.monitorLabelValues, heartbeat.status);
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
try { try {
if (typeof heartbeat.ping === "number") { if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) monitor_response_time.set(this.monitorLabelValues, heartbeat.ping);
} else { } else {
// Is it good? // Is it good?
monitor_response_time.set(this.monitorLabelValues, -1) monitor_response_time.set(this.monitorLabelValues, -1);
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
} }
@ -87,4 +88,4 @@ class Prometheus {
module.exports = { module.exports = {
Prometheus Prometheus
} };

View file

@ -4,15 +4,89 @@ const { R } = require("redbean-node");
const server = require("../server"); const server = require("../server");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, flipStatus, debug } = require("../../src/util");
let router = express.Router(); let router = express.Router();
let cache = apicache.middleware; let cache = apicache.middleware;
let io = server.io;
router.get("/api/entry-page", async (_, response) => { router.get("/api/entry-page", async (_, response) => {
allowDevAllOrigin(response); allowDevAllOrigin(response);
response.json(server.entryPage); response.json(server.entryPage);
}); });
router.get("/api/push/:pushToken", async (request, response) => {
try {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = request.query.ping || null;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken
]);
if (! monitor) {
throw new Error("Monitor not found or not active.");
}
const previousHeartbeat = await R.getRow(`
SELECT status, time FROM heartbeat
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
`, [
monitor.id
]);
let status = UP;
if (monitor.isUpsideDown()) {
status = flipStatus(status);
}
let isFirstBeat = true;
let previousStatus = status;
let duration = 0;
let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc());
if (previousHeartbeat) {
isFirstBeat = false;
previousStatus = previousHeartbeat.status;
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
debug("PreviousStatus: " + previousStatus);
debug("Current Status: " + status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
bean.monitor_id = monitor.id;
bean.status = status;
bean.msg = msg;
bean.ping = ping;
bean.duration = duration;
await R.store(bean);
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, monitor.id, monitor.user_id);
response.json({
ok: true,
});
if (bean.important) {
await Monitor.sendNotification(isFirstBeat, monitor, bean);
}
} catch (e) {
response.json({
ok: false,
msg: e.message
});
}
});
// Status Page Config // Status Page Config
router.get("/api/status-page/config", async (_request, response) => { router.get("/api/status-page/config", async (_request, response) => {
allowDevAllOrigin(response); allowDevAllOrigin(response);

View file

@ -1,4 +1,9 @@
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
const args = require("args-parser")(process.argv);
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
const config = require("./config");
debug(args);
if (! process.env.NODE_ENV) { if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
@ -6,8 +11,6 @@ if (! process.env.NODE_ENV) {
console.log("Node Env: " + process.env.NODE_ENV); console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
console.log("Importing Node libraries"); console.log("Importing Node libraries");
const fs = require("fs"); const fs = require("fs");
const http = require("http"); const http = require("http");
@ -37,7 +40,7 @@ console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, genSecret, allowDevAllOrigin, checkLogin } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -46,32 +49,52 @@ Notification.init();
debug("Importing Database"); debug("Importing Database");
const Database = require("./database"); const Database = require("./database");
debug("Importing Background Jobs");
const { initBackgroundJobs } = require("./jobs");
const { basicAuth } = require("./auth"); const { basicAuth } = require("./auth");
const { login } = require("./auth"); const { login } = require("./auth");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv);
const checkVersion = require("./check-version"); const checkVersion = require("./check-version");
console.info("Version: " + checkVersion.version); console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. // If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::) // Dual-stack support for (::)
const hostname = process.env.HOST || args.host; let hostname = process.env.UPTIME_KUMA_HOST || args.host;
const port = parseInt(process.env.PORT || args.port || 3001);
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) {
hostname = process.env.HOST;
}
if (hostname) {
console.log("Custom hostname: " + hostname);
}
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
// SSL // SSL
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined; const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined; const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
// Demo Mode? // 2FA / notp verification defaults
const demoMode = args["demo"] || false; const twofa_verification_opts = {
"window": 1,
"time": 30
}
if (demoMode) { /**
* Run unit test after the server is ready
* @type {boolean}
*/
const testMode = !!args["test"] || false;
if (config.demoMode) {
console.log("==== Demo Mode ===="); console.log("==== Demo Mode ====");
} }
console.log("Creating express and socket.io instance") console.log("Creating express and socket.io instance");
const app = express(); const app = express();
let server; let server;
@ -91,7 +114,7 @@ const io = new Server(server);
module.exports.io = io; module.exports.io = io;
// Must be after io instantiation // Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client"); const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
app.use(express.json()); app.use(express.json());
@ -181,10 +204,7 @@ exports.entryPage = "dashboard";
console.log("Adding socket handler"); console.log("Adding socket handler");
io.on("connection", async (socket) => { io.on("connection", async (socket) => {
socket.emit("info", { sendInfo(socket);
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
});
totalClient++; totalClient++;
@ -261,7 +281,7 @@ exports.entryPage = "dashboard";
} }
if (data.token) { if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret); let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (verify && verify.delta == 0) {
callback({ callback({
@ -303,6 +323,12 @@ exports.entryPage = "dashboard";
if (user.twofa_status == 0) { if (user.twofa_status == 0) {
let newSecret = await genSecret(); let newSecret = await genSecret();
let encodedSecret = base32.encode(newSecret); let encodedSecret = base32.encode(newSecret);
// Google authenticator doesn't like equal signs
// The fix is found at https://github.com/guyht/notp
// Related issue: https://github.com/louislam/uptime-kuma/issues/486
encodedSecret = encodedSecret.toString().replace(/=/g, "");
let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`; let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [
@ -373,7 +399,7 @@ exports.entryPage = "dashboard";
socket.userID, socket.userID,
]); ]);
let verify = notp.totp.verify(token, user.twofa_secret); let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (verify && verify.delta == 0) {
callback({ callback({
@ -499,6 +525,9 @@ exports.entryPage = "dashboard";
bean.name = monitor.name; bean.name = monitor.name;
bean.type = monitor.type; bean.type = monitor.type;
bean.url = monitor.url; bean.url = monitor.url;
bean.method = monitor.method;
bean.body = monitor.body;
bean.headers = monitor.headers;
bean.interval = monitor.interval; bean.interval = monitor.interval;
bean.retryInterval = monitor.retryInterval; bean.retryInterval = monitor.retryInterval;
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
@ -512,6 +541,7 @@ exports.entryPage = "dashboard";
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server; bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken;
await R.store(bean); await R.store(bean);
@ -639,6 +669,8 @@ exports.entryPage = "dashboard";
}); });
await sendMonitorList(socket); await sendMonitorList(socket);
// Clear heartbeat list on client
await sendImportantHeartbeatList(socket, monitorID, true, true);
} catch (e) { } catch (e) {
callback({ callback({
@ -863,6 +895,8 @@ exports.entryPage = "dashboard";
msg: "Saved" msg: "Saved"
}); });
sendInfo(socket);
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
@ -1020,6 +1054,9 @@ exports.entryPage = "dashboard";
name: monitorListData[i].name, name: monitorListData[i].name,
type: monitorListData[i].type, type: monitorListData[i].type,
url: monitorListData[i].url, url: monitorListData[i].url,
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,
@ -1035,6 +1072,10 @@ exports.entryPage = "dashboard";
notificationIDList: {}, notificationIDList: {},
}; };
if (monitorListData[i].pushToken) {
monitor.pushToken = monitorListData[i].pushToken;
}
let bean = R.dispense("monitor"); let bean = R.dispense("monitor");
let notificationIDList = monitor.notificationIDList; let notificationIDList = monitor.notificationIDList;
@ -1215,8 +1256,14 @@ exports.entryPage = "dashboard";
} }
startMonitors(); startMonitors();
checkVersion.startInterval(); checkVersion.startInterval();
if (testMode) {
startUnitTest();
}
}); });
initBackgroundJobs(args);
})(); })();
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {

View file

@ -5,6 +5,15 @@ 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");
const iconv = require("iconv-lite");
const chardet = require("chardet");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
exports.LIN = /^linux/.test(process.platform);
exports.MAC = /^darwin/.test(process.platform);
exports.FBSD = /^freebsd/.test(process.platform);
/** /**
* Init or reset JWT secret * Init or reset JWT secret
@ -115,7 +124,7 @@ exports.setting = async function (key) {
} }
}; };
exports.setSetting = async function (key, value) { exports.setSetting = async function (key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
]); ]);
@ -123,6 +132,7 @@ exports.setSetting = async function (key, value) {
bean = R.dispense("setting"); bean = R.dispense("setting");
bean.key = key; bean.key = key;
} }
bean.type = type;
bean.value = JSON.stringify(value); bean.value = JSON.stringify(value);
await R.store(bean); await R.store(bean);
}; };
@ -185,38 +195,42 @@ 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,
}; };
}; };
@ -272,16 +286,6 @@ exports.getTotalClientInRoom = (io, roomName) => {
} }
}; };
exports.genSecret = () => {
let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length;
for ( let i = 0; i < 64; i++ ) {
secret += chars.charAt(Math.floor(Math.random() * charsLength));
}
return secret;
};
exports.allowDevAllOrigin = (res) => { exports.allowDevAllOrigin = (res) => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
exports.allowAllOrigin(res); exports.allowAllOrigin(res);
@ -298,3 +302,33 @@ exports.checkLogin = (socket) => {
throw new Error("You are not logged in."); throw new Error("You are not logged in.");
} }
}; };
exports.startUnitTest = async () => {
console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = child_process.spawn(npm, ["run", "jest"]);
child.stdout.on("data", (data) => {
console.log(data.toString());
});
child.stderr.on("data", (data) => {
console.log(data.toString());
});
child.on("close", function (code) {
console.log("Jest exit code: " + code);
process.exit(code);
});
};
/**
* @param body : Buffer
* @returns {string}
*/
exports.convertToUTF8 = (body) => {
const guessEncoding = chardet.detect(body);
//debug("Guess Encoding: " + guessEncoding);
const str = iconv.decode(body, guessEncoding);
return str.toString();
};

View file

@ -3,5 +3,10 @@
</template> </template>
<script> <script>
export default {} import { setPageLocale } from "./util-frontend";
export default {
created() {
setPageLocale();
},
};
</script> </script>

View file

@ -1,8 +1,9 @@
@import "vars.scss"; @import "vars.scss";
@import "multiselect.scss";
@import "node_modules/bootstrap/scss/bootstrap"; @import "node_modules/bootstrap/scss/bootstrap";
#app { #app {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji; font-family: BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
} }
h1 { h1 {
@ -13,6 +14,10 @@ h2 {
font-size: 26px; font-size: 26px;
} }
textarea.form-control {
border-radius: 19px;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }
@ -179,6 +184,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;
@ -233,30 +243,6 @@ h2 {
color: $dark-font-color; color: $dark-font-color;
} }
// Multiselect
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input, .multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
.monitor-list { .monitor-list {
.item { .item {
&:hover { &:hover {
@ -428,3 +414,7 @@ h2 {
.vue-image-crop-upload .vicp-wrap { .vue-image-crop-upload .vicp-wrap {
border-radius: 10px !important; border-radius: 10px !important;
} }
// Localization
@import "localization.scss";

View 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;
}
}

View file

@ -0,0 +1,73 @@
@import "vars.scss";
@import "node_modules/vue-multiselect/dist/vue-multiselect";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
min-height: 38px;
padding: 6px 40px 0 8px;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: $border-radius;
margin-bottom: 0;
padding: 6px 26px 6px 10px;
background: $primary !important;
}
.multiselect__placeholder {
font-size: 1rem;
padding-left: 6px;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
opacity: 0.67;
}
.multiselect__input,
.multiselect__single {
line-height: 14px;
margin-bottom: 0;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input,
.multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
}

View 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>

View 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>

View 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>

View file

@ -186,7 +186,7 @@ export default {
.beat { .beat {
display: inline-block; display: inline-block;
background-color: $primary; background-color: $primary;
border-radius: 50rem; border-radius: $border-radius;
&.empty { &.empty {
background-color: aliceblue; background-color: aliceblue;

View file

@ -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;

View file

@ -19,7 +19,7 @@
<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-9 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 }} {{ item.name }}
@ -28,7 +28,7 @@
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" /> <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</div> </div>
</div> </div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4"> <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" /> <HeartbeatBar size="small" :monitor-id="item.id" />
</div> </div>
</div> </div>
@ -47,6 +47,7 @@
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"; import Tag from "../components/Tag.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default { export default {
components: { components: {
@ -62,7 +63,7 @@ export default {
data() { data() {
return { return {
searchText: "", searchText: "",
} };
}, },
computed: { computed: {
sortedMonitorList() { sortedMonitorList() {
@ -91,7 +92,7 @@ export default {
} }
return m1.name.localeCompare(m2.name); return m1.name.localeCompare(m2.name);
}) });
// Simple filter by search text // Simple filter by search text
// finds monitor name, tag name or tag value // finds monitor name, tag name or tag value
@ -100,8 +101,8 @@ export default {
result = result.filter(monitor => { result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText) return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText)) || tag.value?.toLowerCase().includes(loweredSearchText));
}) });
} }
return result; return result;
@ -109,13 +110,13 @@ export default {
}, },
methods: { methods: {
monitorURL(id) { monitorURL(id) {
return "/dashboard/" + id; return getMonitorRelativeURL(id);
}, },
clearSearchText() { clearSearchText() {
this.searchText = ""; this.searchText = "";
} }
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -13,23 +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="teams">Microsoft Teams</option>
<option value="signal">Signal</option>
<option value="gotify">Gotify</option>
<option value="slack">Slack</option>
<option value="rocket.chat">Rocket.chat</option>
<option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
<option value="line">Line Messenger</option>
<option value="mattermost">Mattermost</option>
</select> </select>
</div> </div>
@ -38,370 +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>
<div class="mb-3">
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
</div>
</template>
<template v-if="notification.type === 'signal'">
<div class="mb-3">
<label for="signal-url" class="form-label">Post URL</label>
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-number" class="form-label">Number</label>
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-recipients" class="form-label">Recipients</label>
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text">
You need to have a signal client with REST API.
<p style="margin-top: 8px;">
You can check this url to view how to setup one:
</p>
<p style="margin-top: 8px;">
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
</p>
<p style="margin-top: 8px;">
IMPORTANT: You cannot mix groups and numbers in recipients!
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'gotify'">
<div class="mb-3">
<label for="gotify-application-token" class="form-label">Application Token</label>
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">Server URL</label>
<div class="input-group mb-3">
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="gotify-priority" class="form-label">Priority</label>
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
</div>
</template>
<template v-if="notification.type === 'slack'">
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label>
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">Channel Name</label>
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'rocket.chat'">
<div class="mb-3">
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
<label for="rocket-username" class="form-label">Username</label>
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
<label for="rocket-channel" class="form-label">Channel Name</label>
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'mattermost'">
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">Username</label>
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
<label for="mattermost-channel" class="form-label">Channel Name</label>
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</p>
<p style="margin-top: 8px;">
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</p>
</template>
<template v-if="notification.type === 'octopush'">
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">SMS Type</label>
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
</select>
<div class="form-text">
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
</div>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</p>
</template>
<template v-if="notification.type === 'pushover'">
<div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-device" class="form-label">Device</label>
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label>
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
<label for="pushover-priority" class="form-label">Priority</label>
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
<option>-2</option>
<option>-1</option>
<option>0</option>
<option>1</option>
<option>2</option>
</select>
<label for="pushover-sound" class="form-label">Notification Sound</label>
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
<option>pushover</option>
<option>bike</option>
<option>bugle</option>
<option>cashregister</option>
<option>classical</option>
<option>cosmic</option>
<option>falling</option>
<option>gamelan</option>
<option>incoming</option>
<option>intermission</option>
<option>mechanical</option>
<option>pianobar</option>
<option>siren</option>
<option>spacealarm</option>
<option>tugboat</option>
<option>alien</option>
<option>climb</option>
<option>persistent</option>
<option>echo</option>
<option>updown</option>
<option>vibrate</option>
<option>none</option>
</select>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p>
<p style="margin-top: 8px;">
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
</p>
<p style="margin-top: 8px;">
If you want to send notifications to different devices, fill out Device field.
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'apprise'">
<div class="mb-3">
<label for="apprise-url" class="form-label">Apprise URL</label>
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
<div class="form-text">
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
<p>
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
</p>
</div>
</div>
<div class="mb-3">
<p>
Status:
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
</p>
</div>
</template>
<template v-if="notification.type === 'lunasea'">
<div class="mb-3">
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>Required</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushbullet'">
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">Access Token</label>
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</p>
</template>
<template v-if="notification.type === 'line'">
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">Channel access token</label>
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="form-text">
Line Developers Console - <b>Basic Settings</b>
</div>
<div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label>
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Messaging API</b>
</div>
<div class="form-text" style="margin-top: 8px;">
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
</div>
</template>
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
<Teams v-if="notification.type === 'teams'" />
<div class="mb-3 mt-4"> <div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4"> <hr class="dropdown-divider mb-4">
@ -446,22 +68,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Modal } from "bootstrap" 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 Teams from "./notifications/Teams.vue";
import SMTP from "./notifications/SMTP.vue";
export default { export default {
components: { components: {
Confirm, Confirm,
HiddenInput,
Telegram,
Teams,
SMTP,
}, },
props: {}, props: {},
emits: ["added"], emits: ["added"],
@ -470,43 +85,48 @@ 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];
} }
}, },
watch: { watch: {
"notification.type"(to, from) { "notification.type"(to, from) {
let oldName; let oldName;
if (from) { if (from) {
oldName = `My ${ucfirst(from)} Alert (1)`; oldName = this.getUniqueDefaultName(from);
} else { } else {
oldName = ""; oldName = "";
} }
if (! this.notification.name || this.notification.name === oldName) { if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)` this.notification.name = this.getUniqueDefaultName(to);
} }
}, },
}, },
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: {
deleteConfirm() { deleteConfirm() {
this.modal.hide(); this.modal.hide();
this.$refs.confirmDelete.show() this.$refs.confirmDelete.show();
}, },
show(notificationID) { show(notificationID) {
@ -525,21 +145,19 @@ export default {
name: "", name: "",
type: null, type: null,
isDefault: false, isDefault: false,
} };
// 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();
}, },
submit() { submit() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => { this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
this.$root.toastRes(res) this.$root.toastRes(res);
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {
@ -551,30 +169,45 @@ export default {
} }
} }
}) });
}, },
test() { test() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("testNotification", this.notification, (res) => { this.$root.getSocket().emit("testNotification", this.notification, (res) => {
this.$root.toastRes(res) this.$root.toastRes(res);
this.processing = false; this.processing = false;
}) });
}, },
deleteNotification() { deleteNotification() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("deleteNotification", this.id, (res) => { this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
this.$root.toastRes(res) this.$root.toastRes(res);
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {
this.modal.hide() this.modal.hide();
} }
}) });
},
}, },
/**
* @param {keyof NotificationFormList} notificationKey
* @return {string}
*/
getUniqueDefaultName(notificationKey) {
let index = 1;
let name = "";
do {
name = this.$t("defaultNotificationName", {
notification: this.$t(notificationKey).replace(/\(.+\)/, "").trim(),
number: index++
});
} while (this.$root.notificationList.find(it => it.name === name));
return name;
} }
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -11,18 +11,18 @@ export default {
computed: { computed: {
color() { color() {
if (this.status === 0) { if (this.status === 0) {
return "danger" return "danger";
} }
if (this.status === 1) { if (this.status === 1) {
return "primary" return "primary";
} }
if (this.status === 2) { if (this.status === 2) {
return "warning" return "warning";
} }
return "secondary" return "secondary";
}, },
text() { text() {
@ -41,11 +41,11 @@ export default {
return this.$t("Unknown"); return this.$t("Unknown");
}, },
}, },
} };
</script> </script>
<style scoped> <style scoped>
span { span {
width: 64px; min-width: 64px;
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<h4 class="mb-3">{{ $t("Tags") }}</h4> <h4 class="mt-5 mb-3">{{ $t("Tags") }}</h4>
<div class="mb-3 p-1"> <div v-if="selectedTags.length > 0" class="mb-2 p-1">
<tag <tag
v-for="item in selectedTags" v-for="item in selectedTags"
:key="item.id" :key="item.id"
@ -124,8 +124,8 @@
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast();
export default { export default {
components: { components: {
@ -186,7 +186,7 @@ export default {
color: "#7C3AED" }, color: "#7C3AED" },
{ name: this.$t("Pink"), { name: this.$t("Pink"),
color: "#DB2777" }, color: "#DB2777" },
] ];
}, },
validateDraftTag() { validateDraftTag() {
let nameInvalid = false; let nameInvalid = false;
@ -227,7 +227,7 @@ export default {
invalid, invalid,
nameInvalid, nameInvalid,
valueInvalid, valueInvalid,
} };
}, },
}, },
mounted() { mounted() {
@ -243,7 +243,7 @@ export default {
if (res.ok) { if (res.ok) {
this.existingTags = res.tags; this.existingTags = res.tags;
} else { } else {
toast.error(res.msg) toast.error(res.msg);
} }
}); });
}, },
@ -277,7 +277,7 @@ export default {
name: this.newDraftTag.select.name, name: this.newDraftTag.select.name,
value: this.newDraftTag.value, value: this.newDraftTag.value,
new: true, new: true,
}) });
} }
} else { } else {
// Add new Tag // Add new Tag
@ -286,7 +286,7 @@ export default {
name: this.newDraftTag.name.trim(), name: this.newDraftTag.name.trim(),
value: this.newDraftTag.value, value: this.newDraftTag.value,
new: true, new: true,
}) });
} }
this.clearDraftTag(); this.clearDraftTag();
}, },
@ -348,7 +348,7 @@ export default {
if (tag.name == newTag.name && tag.color == newTag.color) { if (tag.name == newTag.name && tag.color == newTag.color) {
tag.id = newTagResult.id; tag.id = newTagResult.id;
} }
}) });
} else { } else {
tagId = newTag.id; tagId = newTag.id;
} }

View file

@ -0,0 +1,25 @@
<template>
<div class="mb-3">
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
<div class="form-text">
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,35 @@
<template>
<div class="mb-3">
<label for="apprise-url" class="form-label">{{ $t("Apprise URL") }}</label>
<input id="apprise-url" v-model="$parent.notification.appriseURL" type="text" class="form-control" required>
<div class="form-text">
<p>{{ $t("Example:", ["twilio://AccountSid:AuthToken@FromPhoneNo"]) }}</p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
</i18n-t>
</div>
</div>
<div class="mb-3">
<i18n-t tag="p" keypath="Status:">
<span v-if="appriseInstalled" class="text-primary">{{ $t("appriseInstalled") }}</span>
<i18n-t v-else tag="span" keypath="appriseNotInstalled" class="text-danger">
<a href="https://github.com/caronc/apprise" target="_blank">{{ $t("Read more") }}</a>
</i18n-t>
</i18n-t>
</div>
</template>
<script>
export default {
data() {
return {
appriseInstalled: false
};
},
mounted() {
this.$root.getSocket().emit("checkApprise", (installed) => {
this.appriseInstalled = installed;
});
},
};
</script>

View file

@ -0,0 +1,16 @@
<template>
<div class="mb-3">
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
<div class="form-text">
<p>For safety, must use secret key</p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,19 @@
<template>
<div class="mb-3">
<label for="discord-webhook-url" class="form-label">{{ $t("Discord Webhook URL") }}</label>
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
<div class="form-text">
{{ $t("wayToGetDiscordURL") }}
</div>
</div>
<div class="mb-3">
<label for="discord-username" class="form-label">{{ $t("Bot Display Name") }}</label>
<input id="discord-username" v-model="$parent.notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div>
<div class="mb-3">
<label for="discord-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" :placeholder="$t('Hello @everyone is...')">
</div>
</template>

View file

@ -0,0 +1,15 @@
<template>
<div class="mb-3">
<label for="Feishu-WebHookUrl" class="form-label">{{ $t("Feishu WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="Feishu-WebHookUrl" v-model="$parent.notification.feishuWebHookUrl" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div>
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
<a
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
target="_blank"
>{{ $t("here") }}</a>
</i18n-t>
</div>
</template>

View file

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="gotify-priority" class="form-label">{{ $t("Priority") }}</label>
<input id="gotify-priority" v-model="$parent.notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.gotifyPriority === "undefined") {
this.$parent.notification.gotifyPriority = 8;
}
},
}
</script>

View file

@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
<b>{{ $t("Basic Settings") }}</b>
</i18n-t>
<div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label>
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
</div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
<b>{{ $t("Messaging API") }}</b>
</i18n-t>
<i18n-t tag="div" keypath="wayToGetLineChannelToken" class="form-text" style="margin-top: 8px;">
<a href="https://developers.line.biz/console/" target="_blank">{{ $t("Line Developers Console") }}</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,9 @@
<template>
<div class="mb-3">
<label for="lunasea-device" class="form-label">{{ $t("LunaSea Device ID") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div>
</div>
</template>

View file

@ -0,0 +1,34 @@
<template>
<div class="mb-3">
<label for="homeserver-url" class="form-label">{{ $t("matrixHomeserverURL") }}</label><span style="color: red;"><sup>*</sup></span>
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
</div>
<div class="mb-3">
<label for="internal-room-id" class="form-label">{{ $t("Internal Room Id") }}</label><span style="color: red;"><sup>*</sup></span>
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
</div>
<div class="mb-3">
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<p style="margin-top: 8px;">
{{ $t("matrixDesc1") }}
</p>
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
</i18n-t>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color:red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">{{ $t("Username") }}</label>
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
<label for="mattermost-iconurl" class="form-label">{{ $t("Icon URL") }}</label>
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
<label for="mattermost-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
<label for="mattermost-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("aboutMattermostChannelName") }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutKumaURL") }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutIconURL") }}
</p>
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,50 @@
<template>
<div class="mb-3">
<label for="octopush-version" class="form-label">Octopush API Version</label>
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
<option value="2">Octopush (endpoint: api.octopush.com)</option>
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
</select>
<div class="form-text">
{{ $t("octopushLegacyHint") }}
</div>
</div>
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
<select id="octopush-type-sms" v-model="$parent.notification.octopushSMSType" class="form-select">
<option value="sms_premium">{{ $t("octopushTypePremium") }}</option>
<option value="sms_low_cost">{{ $t("octopushTypeLowCost") }}</option>
</select>
<i18n-t tag="div" keypath="Check octopush prices" class="form-text">
<a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">{{ $t("octopushPhoneNumber") }}</label>
<input id="octopush-phone-number" v-model="$parent.notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">{{ $t("octopushSMSSender") }}</label>
<input id="octopush-sender-name" v-model="$parent.notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,39 @@
<template>
<div class="mb-3">
<label for="promosms-login" class="form-label">API LOGIN</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">API PASSWORD</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
<option value="0">{{ $t("promosmsTypeFlash") }}</option>
<option value="1">{{ $t("promosmsTypeEco") }}</option>
<option value="2">{{ $t("promosmsTypeFull") }}</option>
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
</select>
<div class="form-text">
{{ $t("checkPrice", [$t("promosms")]) }}
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
</div>
</div>
<div class="mb-3">
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
<input id="promosms-phone-number" v-model="$parent.notification.promosmsPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,20 @@
<template>
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,67 @@
<template>
<div class="mb-3">
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>
<input id="pushover-title" v-model="$parent.notification.pushovertitle" type="text" class="form-control">
<label for="pushover-priority" class="form-label">{{ $t("Priority") }}</label>
<select id="pushover-priority" v-model="$parent.notification.pushoverpriority" class="form-select">
<option>-2</option>
<option>-1</option>
<option>0</option>
<option>1</option>
<option>2</option>
</select>
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
<option>pushover</option>
<option>bike</option>
<option>bugle</option>
<option>cashregister</option>
<option>classical</option>
<option>cosmic</option>
<option>falling</option>
<option>gamelan</option>
<option>incoming</option>
<option>intermission</option>
<option>mechanical</option>
<option>pianobar</option>
<option>siren</option>
<option>spacealarm</option>
<option>tugboat</option>
<option>alien</option>
<option>climb</option>
<option>persistent</option>
<option>echo</option>
<option>updown</option>
<option>vibrate</option>
<option>none</option>
</select>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("pushoverDesc1") }}
</p>
<p style="margin-top: 8px;">
{{ $t("pushoverDesc2") }}
</p>
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
}
</script>

View file

@ -0,0 +1,26 @@
<template>
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View file

@ -0,0 +1,27 @@
<template>
<div class="mb-3">
<label for="rocket-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="rocket-webhook-url" v-model="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
<label for="rocket-username" class="form-label">{{ $t("Username") }}</label>
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
<label for="rocket-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
<label for="rocket-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="rocket-channel-name" v-model="$parent.notification.rocketchannel" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutKumaURL") }}
</p>
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -12,8 +12,8 @@
<div class="mb-3"> <div class="mb-3">
<label for="secure" class="form-label">Secure</label> <label for="secure" class="form-label">Secure</label>
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select"> <select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
<option :value="false">None / STARTTLS (25, 587)</option> <option :value="false">{{ $t("secureOptionNone") }}</option>
<option :value="true">TLS (465)</option> <option :value="true">{{ $t("secureOptionTLS") }}</option>
</select> </select>
</div> </div>
@ -21,7 +21,7 @@
<div class="form-check"> <div class="form-check">
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value=""> <input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls-error"> <label class="form-check-label" for="ignore-tls-error">
Ignore TLS Error {{ $t("Ignore TLS Error") }}
</label> </label>
</div> </div>
</div> </div>
@ -37,25 +37,37 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="from-email" class="form-label">From Email</label> <label for="from-email" class="form-label">{{ $t("From Email") }}</label>
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder="&quot;Uptime Kuma&quot; &lt;example@kuma.pet&gt;"> <input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder="&quot;Uptime Kuma&quot; &lt;example@kuma.pet&gt;">
<div class="form-text"> <div class="form-text">
</div> </div>
</div> </div>
<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">{{ $t("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">{{ $t("smtpCC") }}</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">{{ $t("smtpBCC") }}</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 class="mb-3">
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
<div v-pre class="form-text">
(leave blank for default one)<br />
{{NAME}}: Service Name<br />
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
{{URL}}: URL<br />
{{STATUS}}: Status<br />
</div>
</div> </div>
</template> </template>
@ -66,10 +78,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>

View file

@ -0,0 +1,34 @@
<template>
<div class="mb-3">
<label for="signal-url" class="form-label">{{ $t("Post URL") }}</label>
<input id="signal-url" v-model="$parent.notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-number" class="form-label">{{ $t("Number") }}</label>
<input id="signal-number" v-model="$parent.notification.signalNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-recipients" class="form-label">{{ $t("Recipients") }}</label>
<input id="signal-recipients" v-model="$parent.notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text">
<p style="margin-top: 8px;">
{{ $t("needSignalAPI") }}
</p>
<p style="margin-top: 8px;">
{{ $t("wayToCheckSignalURL") }}
</p>
<p style="margin-top: 8px;">
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
</p>
<p style="margin-top: 8px;">
{{ $t("signalImportant") }}
</p>
</div>
</div>
</template>

View file

@ -0,0 +1,28 @@
<template>
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("aboutChannelName", [$t("slack")]) }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutKumaURL") }}
</p>
<i18n-t tag="p" keypath="emojiCheatSheet" style="margin-top: 8px;">
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="teams-webhookurl" class="form-label">Webhook URL</label> <label for="teams-webhookurl" class="form-label">{{ $t("Webhook URL") }}</label>
<input <input
id="teams-webhookurl" id="teams-webhookurl"
v-model="$parent.notification.webhookUrl" v-model="$parent.notification.webhookUrl"
@ -8,22 +8,11 @@
class="form-control" class="form-control"
required required
/> />
<div class="form-text"> <i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
You can learn how to create a webhook url
<a <a
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook" href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
target="_blank" target="_blank"
>here</a>. >{{ $t("here") }}</a>
</div> </i18n-t>
</div> </div>
</template> </template>
<script>
export default {
data() {
return {
name: "teams",
};
},
};
</script>

View file

@ -1,14 +1,14 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label> <label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<div class="form-text"> <i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>. <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
</div> </i18n-t>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label> <label for="telegram-chat-id" class="form-label">{{ $t("Chat ID") }}</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required> <input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required>
@ -18,10 +18,10 @@
</div> </div>
<div class="form-text"> <div class="form-text">
Support Direct Chat / Group / Channel's Chat ID {{ $t("supportTelegramChatID") }}
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id: {{ $t("wayToGetTelegramChatID") }}
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
@ -47,14 +47,9 @@ export default {
components: { components: {
HiddenInput, HiddenInput,
}, },
data() {
return {
name: "telegram",
}
},
computed: { computed: {
telegramGetUpdatesURL() { telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>" let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`
if (this.$parent.notification.telegramBotToken) { if (this.$parent.notification.telegramBotToken) {
token = this.$parent.notification.telegramBotToken; token = this.$parent.notification.telegramBotToken;
@ -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() {
@ -79,11 +71,11 @@ export default {
} else if (update.message) { } else if (update.message) {
this.notification.telegramChatID = update.message.chat.id; this.notification.telegramChatID = update.message.chat.id;
} else { } else {
throw new Error("Chat ID is not found, please send a message to this bot first") throw new Error(this.$t("chatIDNotFound"))
} }
} else { } else {
throw new Error("Chat ID is not found, please send a message to this bot first") throw new Error(this.$t("chatIDNotFound"))
} }
} catch (error) { } catch (error) {

View file

@ -0,0 +1,28 @@
<template>
<div class="mb-3">
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
<input id="webhook-url" v-model="$parent.notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="webhook-content-type" class="form-label">{{ $t("Content Type") }}</label>
<select id="webhook-content-type" v-model="$parent.notification.webhookContentType" class="form-select" required>
<option value="json">
application/json
</option>
<option value="form-data">
multipart/form-data
</option>
</select>
<div class="form-text">
<p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
<i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>"multipart/form-data"</template>
<template #decodeFunction>
<strong>json_decode($_POST['data'])</strong>
</template>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,54 @@
import STMP from "./SMTP.vue"
import Telegram from "./Telegram.vue";
import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue";
import Signal from "./Signal.vue";
import Gotify from "./Gotify.vue";
import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue";
import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue";
import Octopush from "./Octopush.vue";
import PromoSMS from "./PromoSMS.vue";
import LunaSea from "./LunaSea.vue";
import Feishu from "./Feishu.vue";
import Apprise from "./Apprise.vue";
import Pushbullet from "./Pushbullet.vue";
import Line from "./Line.vue";
import Mattermost from "./Mattermost.vue";
import Matrix from "./Matrix.vue";
import AliyunSMS from "./AliyunSms.vue";
import DingDing from "./DingDing.vue";
/**
* Manage all notification form.
*
* @type { Record<string, any> }
*/
const NotificationFormList = {
"telegram": Telegram,
"webhook": Webhook,
"smtp": STMP,
"discord": Discord,
"teams": Teams,
"signal": Signal,
"gotify": Gotify,
"slack": Slack,
"rocket.chat": RocketChat,
"pushover": Pushover,
"pushy": Pushy,
"octopush": Octopush,
"promosms": PromoSMS,
"lunasea": LunaSea,
"Feishu": Feishu,
"AliyunSMS": AliyunSMS,
"apprise": Apprise,
"pushbullet": Pushbullet,
"line": Line,
"mattermost": Mattermost,
"matrix": Matrix,
"DingDing": DingDing
}
export default NotificationFormList

View file

@ -1,33 +1,43 @@
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n/index";
import daDK from "./languages/da-DK"; import daDK from "./languages/da-DK";
import deDE from "./languages/de-DE"; import deDE from "./languages/de-DE";
import en from "./languages/en"; import en from "./languages/en";
import esEs from "./languages/es-ES"; import esEs from "./languages/es-ES";
import ptBR from "./languages/pt-BR";
import etEE from "./languages/et-EE"; import etEE from "./languages/et-EE";
import fa from "./languages/fa";
import frFR from "./languages/fr-FR"; import frFR from "./languages/fr-FR";
import hu from "./languages/hu";
import itIT from "./languages/it-IT"; import itIT from "./languages/it-IT";
import idID from "./languages/id-ID";
import ja from "./languages/ja"; import ja from "./languages/ja";
import koKR from "./languages/ko-KR"; import koKR from "./languages/ko-KR";
import nlNL from "./languages/nl-NL"; import nlNL from "./languages/nl-NL";
import nbNO from "./languages/nb-NO";
import pl from "./languages/pl"; import pl from "./languages/pl";
import ptBR from "./languages/pt-BR";
import bgBG from "./languages/bg-BG";
import ruRU from "./languages/ru-RU"; import ruRU from "./languages/ru-RU";
import sr from "./languages/sr"; import sr from "./languages/sr";
import srLatn from "./languages/sr-latn"; import srLatn from "./languages/sr-latn";
import trTR from "./languages/tr-TR";
import svSE from "./languages/sv-SE"; import svSE from "./languages/sv-SE";
import trTR from "./languages/tr-TR";
import zhCN from "./languages/zh-CN"; import zhCN from "./languages/zh-CN";
import zhHK from "./languages/zh-HK"; import zhHK from "./languages/zh-HK";
const languageList = { const languageList = {
en, en,
"zh-HK": zhHK, "zh-HK": zhHK,
"bg-BG": bgBG,
"de-DE": deDE, "de-DE": deDE,
"nl-NL": nlNL, "nl-NL": nlNL,
"nb-NO": nbNO,
"es-ES": esEs, "es-ES": esEs,
"fa": fa,
"pt-BR": ptBR, "pt-BR": ptBR,
"fr-FR": frFR, "fr-FR": frFR,
"hu": hu,
"it-IT": itIT, "it-IT": itIT,
"id-ID" : idID,
"ja": ja, "ja": ja,
"da-DK": daDK, "da-DK": daDK,
"sr": sr, "sr": sr,
@ -41,8 +51,19 @@ const languageList = {
"et-EE": etEE, "et-EE": etEE,
}; };
const rtlLangs = ["fa"];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language
|| languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
|| "en";
export const localeDirection = () => {
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";
};
export const i18n = createI18n({ export const i18n = createI18n({
locale: localStorage.locale || "en", locale: currentLocale(),
fallbackLocale: "en", fallbackLocale: "en",
silentFallbackWarn: true, silentFallbackWarn: true,
silentTranslationWarn: true, silentTranslationWarn: true,

View file

@ -26,7 +26,13 @@ import {
faArrowsAltV, faArrowsAltV,
faUnlink, faUnlink,
faQuestionCircle, faQuestionCircle,
faImages, faUpload, faImages,
faUpload,
faCopy,
faCheck,
faFile,
faAward,
faLink,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -54,6 +60,11 @@ library.add(
faQuestionCircle, faQuestionCircle,
faImages, faImages,
faUpload, faUpload,
faCopy,
faCheck,
faFile,
faAward,
faLink,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

200
src/languages/bg-BG.js Normal file
View file

@ -0,0 +1,200 @@
export default {
languageName: "Български",
checkEverySecond: "Ще се извършва на всеки {0} секунди",
retryCheckEverySecond: "Ще се извършва на всеки {0} секунди",
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие",
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.",
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
passwordNotMatchMsg: "Повторената парола не съвпада.",
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.",
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
resoverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известяване със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известяване.",
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?",
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?",
Settings: "Настройки",
Dashboard: "Табло",
"New Update": "Налична е актуализация",
Language: "Език",
Appearance: "Изглед",
Theme: "Тема",
General: "Общи",
Version: "Версия",
"Check Update On GitHub": "Проверка за актуализация в GitHub",
List: "Списък",
Add: "Добави",
"Add New Monitor": "Добави монитор",
"Quick Stats": "Кратка статистика",
Up: "Достъпен",
Down: "Недостъпен",
Pending: "Изчаква",
Unknown: "Неизвестен",
Pause: "Пауза",
Name: "Име",
Status: "Статус",
DateTime: "Дата и час",
Message: "Отговор",
"No important events": "Няма важни събития",
Resume: "Възобнови",
Edit: "Редактирай",
Delete: "Изтрий",
Current: "Текущ",
Uptime: "Достъпност",
"Cert Exp.": "Вал. сертификат",
days: "дни",
day: "ден",
"-day": "-дни",
hour: "час",
"-hour": "-часa",
Response: "Отговор",
Ping: "Пинг",
"Monitor Type": "Монитор тип",
Keyword: "Ключова дума",
"Friendly Name": "Псевдоним",
URL: "URL Адрес",
Hostname: "Име на хост",
Port: "Порт",
"Heartbeat Interval": "Честота на проверка",
Retries: "Повторни опити",
"Heartbeat Retry Interval": "Честота на повторните опити",
Advanced: "Разширени",
"Upside Down Mode": "Обърнат режим",
"Max. Redirects": "Макс. брой пренасочвания",
"Accepted Status Codes": "Допустими статус кодове",
Save: "Запази",
Notifications: "Известявания",
"Not available, please setup.": "Не е налично. Моля, настройте.",
"Setup Notification": "Настройки за известявания",
Light: "Светла",
Dark: "Тъмна",
Auto: "Автоматично",
"Theme - Heartbeat Bar": "Тема - поле проверки",
Normal: "Нормално",
Bottom: "Долу",
None: "Без",
Timezone: "Часова зона",
"Search Engine Visibility": "Видимост за търсачки",
"Allow indexing": "Разреши индексиране",
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките",
"Change Password": "Промени парола",
"Current Password": "Текуща парола",
"New Password": "Нова парола",
"Repeat New Password": "Повторете новата парола",
"Update Password": "Актуализирай парола",
"Disable Auth": "Изключи удостоверяване",
"Enable Auth": "Включи удостоверяване",
Logout: "Изход от профила",
Leave: "Отказ",
"I understand, please disable": "Разбирам. Моля, изключи",
Confirm: "Потвърдете",
Yes: "Да",
No: "Не",
Username: "Потребител",
Password: "Парола",
"Remember me": "Запомни ме",
Login: "Вход",
"No Monitors, please": "Моля, без монитори",
"add one": "добави един",
"Notification Type": "Тип известяване",
Email: "Имейл",
Test: "Тест",
"Certificate Info": "Информация за сертификат",
"Resolver Server": "Преобразуващ (DNS) сървър",
"Resource Record Type": "Тип запис",
"Last Result": "Последен резултат",
"Create your admin account": "Създаване на администриращ акаунт",
"Repeat Password": "Повторете паролата",
"Import Backup": "Импорт на архив",
"Export Backup": "Експорт на архив",
Export: "Експорт",
Import: "Импорт",
respTime: "Време за отговор (ms)",
notAvailableShort: "Няма",
"Default enabled": "Включен по подразбиране",
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
Create: "Създай",
"Clear Data": "Изтрий данни",
Events: "Събития",
Heartbeats: "Проверки",
"Auto Get": "Автоматияно получаване",
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
backupDescription2: "PS: Данни за история и събития не са включени.",
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
alertNoFile: "Моля, изберете файл за импортиране.",
alertWrongFileType: "Моля, изберете JSON файл.",
"Clear all statistics": "Изтрий цялата статистика",
"Skip existing": "Пропусни съществуващите",
Overwrite: "Презапиши",
Options: "Опции",
"Keep both": "Запази двете",
"Verify Token": "Проверка на токен код",
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включи 2FA",
"Disable 2FA": "Изключи 2FA",
"2FA Settings": "Настройки 2FA",
"Two Factor Authentication": "Двуфакторно удостоверяване",
Active: "Активно",
Inactive: "Неактивно",
Token: "Токен код",
"Show URI": "Покажи URI",
Tags: "Етикети",
"Add New below or Select...": "Добавете нов по-долу или изберете...",
"Tag with this name already exist.": "Етикет с това име вече съществува.",
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.",
color: "цвят",
"value (optional)": "стойност (по желание)",
Gray: "Сиво",
Red: "Червено",
Orange: "Оранжево",
Green: "Зелено",
Blue: "Синьо",
Indigo: "Индиго",
Purple: "Лилаво",
Pink: "Розово",
"Search...": "Търси...",
"Avg. Ping": "Ср. пинг",
"Avg. Response": "Ср. отговор",
"Entry Page": "Основна страница",
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.",
"No Services": "Няма Услуги",
"All Systems Operational": "Всички услуги са достъпни",
"Partially Degraded Service": "Част от услугите са недостъпни",
"Degraded Service": "Всички услуги са недостъпни",
"Add Group": "Добави група",
"Add a monitor": "Добави монитор",
"Edit Status Page": "Редактиране Статус страница",
"Go to Dashboard": "Към Таблото",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Поддържа 50+ услуги за инвестяване)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Статус страница",
};

View file

@ -1,5 +1,5 @@
export default { export default {
languageName: "Danish", languageName: "Danish (Danmark)",
Settings: "Indstillinger", Settings: "Indstillinger",
Dashboard: "Dashboard", Dashboard: "Dashboard",
"New Update": "Opdatering tilgængelig", "New Update": "Opdatering tilgængelig",
@ -126,57 +126,76 @@ export default {
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.", 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: "Retry every {0} seconds.", retryCheckEverySecond: "Prøv igen hvert {0} sekund.",
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.", 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: "Are you sure to import the backup? Please make sure you've selected the right import option.", confirmImportMsg: "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Heartbeat Retry Interval": "Heartbeat Gentagelsesinterval",
"Import Backup": "Import Backup", "Import Backup": "Importer Backup",
"Export Backup": "Export Backup", "Export Backup": "Eksporter Backup",
"Skip existing": "Skip existing", "Skip existing": "Spring over eksisterende",
Overwrite: "Overwrite", Overwrite: "Overskriv",
Options: "Options", Options: "Valgmuligheder",
"Keep both": "Keep both", "Keep both": "Behold begge",
Tags: "Tags", Tags: "Tags",
"Add New below or Select...": "Add New below or Select...", "Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
"Tag with this name already exist.": "Tag with this name already exist.", "Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
"Tag with this value already exist.": "Tag with this value already exist.", "Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
color: "color", color: "farve",
"value (optional)": "value (optional)", "value (optional)": "værdi (valgfri)",
Gray: "Gray", Gray: "Grå",
Red: "Red", Red: "Rød",
Orange: "Orange", Orange: "Orange",
Green: "Green", Green: "Grøn",
Blue: "Blue", Blue: "Blå",
Indigo: "Indigo", Indigo: "Indigo",
Purple: "Purple", Purple: "Lilla",
Pink: "Pink", Pink: "Pink",
"Search...": "Search...", "Search...": "Søg...",
"Avg. Ping": "Avg. Ping", "Avg. Ping": "Gns. Ping",
"Avg. Response": "Avg. Response", "Avg. Response": "Gns. Respons",
"Entry Page": "Entry Page", "Entry Page": "Entry Side",
"statusPageNothing": "Nothing here, please add a group or a monitor.", statusPageNothing: "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
"No Services": "No Services", "No Services": "Ingen Tjenester",
"All Systems Operational": "All Systems Operational", "All Systems Operational": "Alle Systemer i Drift",
"Partially Degraded Service": "Partially Degraded Service", "Partially Degraded Service": "Delvist Forringet Service",
"Degraded Service": "Degraded Service", "Degraded Service": "Forringet Service",
"Add Group": "Add Group", "Add Group": "Tilføj Gruppe",
"Add a monitor": "Add a monitor", "Add a monitor": "Tilføj en Overvåger",
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Rediger Statusside",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Gå til Dashboard",
"Status Page": "Status Page",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
}; };

View file

@ -1,14 +1,14 @@
export default { export default {
languageName: "German", languageName: "Deutsch (Deutschland)",
Settings: "Einstellungen", Settings: "Einstellungen",
Dashboard: "Dashboard", Dashboard: "Dashboard",
"New Update": "Update Verfügbar", "New Update": "Update verfügbar",
Language: "Sprache", Language: "Sprache",
Appearance: "Erscheinung", Appearance: "Erscheinung",
Theme: "Thema", Theme: "Thema",
General: "Allgemein", General: "Allgemein",
Version: "Version", Version: "Version",
"Check Update On GitHub": "Überprüfen von Updates auf Github", "Check Update On GitHub": "Auf GitHub nach Updates suchen",
List: "Liste", List: "Liste",
Add: "Hinzufügen", Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor", "Add New Monitor": "Neuer Monitor",
@ -38,21 +38,21 @@ export default {
checkEverySecond: "Überprüfe alle {0} Sekunden", checkEverySecond: "Überprüfe alle {0} Sekunden",
Response: "Antwortzeit", Response: "Antwortzeit",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Monitor Typ", "Monitor Type": "Monitor-Typ",
Keyword: "Schlüsselwort", Keyword: "Suchwort",
"Friendly Name": "Anzeigename", "Friendly Name": "Anzeigename",
URL: "URL", URL: "URL",
Hostname: "Hostname", Hostname: "Hostname",
Port: "Port", Port: "Port",
"Heartbeat Interval": "Taktintervall", "Heartbeat Interval": "Prüfintervall",
Retries: "Wiederholungen", Retries: "Wiederholungen",
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.", retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert", Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten", ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Umgedrehter Modus", "Upside Down Mode": "Invertierter Modus",
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.", upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Max. Redirects": "Max. Weiterleitungen", "Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.", maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes", "Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.", acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
Save: "Speichern", Save: "Speichern",
@ -62,27 +62,27 @@ export default {
Light: "Hell", Light: "Hell",
Dark: "Dunkel", Dark: "Dunkel",
Auto: "Auto", Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Taktleiste", "Theme - Heartbeat Bar": "Thema - Zeitleiste",
Normal: "Normal", Normal: "Normal",
Bottom: "Unten", Bottom: "Unten",
None: "Keine", None: "Keine",
Timezone: "Zeitzone", Timezone: "Zeitzone",
"Search Engine Visibility": "Suchmaschinensichtbarkeit", "Search Engine Visibility": "Sichtbarkeit für Suchmaschinen",
"Allow indexing": "Indizierung zulassen", "Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab", "Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
"Change Password": "Passwort ändern", "Change Password": "Passwort ändern",
"Current Password": "Dezeitiges Passwort", "Current Password": "Derzeitiges Passwort",
"New Password": "Neues Passwort", "New Password": "Neues Passwort",
"Repeat New Password": "Wiederhole neues Passwort", "Repeat New Password": "Neues Passwort wiederholen",
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ", passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
"Update Password": "Ändere Passwort", "Update Password": "Passwort aktualisieren",
"Disable Auth": "Authentifizierung deaktivieren", "Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren", "Enable Auth": "Authentifizierung aktivieren",
Logout: "Ausloggen", Logout: "Ausloggen",
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.", notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
Leave: "Verlassen", Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren", "I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätige", Confirm: "Bestätigen",
Yes: "Ja", Yes: "Ja",
No: "Nein", No: "Nein",
Username: "Benutzername", Username: "Benutzername",
@ -91,30 +91,30 @@ export default {
Login: "Einloggen", Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte", "No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen", "add one": "hinzufügen",
"Notification Type": "Benachrichtigungs Dienst", "Notification Type": "Benachrichtigungsdienst",
Email: "E-Mail", Email: "E-Mail",
Test: "Test", Test: "Test",
"Certificate Info": "Zertifikatsinfo", "Certificate Info": "Zertifikatsinfo",
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.", keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?", deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?", deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.", resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
"Resolver Server": "Auflösungsserver", "Resolver Server": "Auflösungsserver",
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.", rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis", "Last Result": "Letztes Ergebnis",
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?", pauseMonitorMsg: "Bist du sicher, dass du den Monitor pausieren möchtest?",
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?", clearEventsMsg: "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?", clearHeartbeatsMsg: "Bist du sicher, dass du alle Statistiken für diesen Monitor löschen möchtest?",
"Clear Data": "Lösche Daten", "Clear Data": "Lösche Daten",
Events: "Ereignisse", Events: "Ereignisse",
Heartbeats: "Statistiken", Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?", confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
"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",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
respTime: "Antw. Zeit (ms)", respTime: "Antw.-Zeit (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert", "Default enabled": "Standardmäßig aktiviert",
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden", "Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
@ -124,35 +124,35 @@ export default {
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.", backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.", backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
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", "Clear all statistics": "Lösche alle Statistiken",
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.", importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder jede Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
"Skip existing": "Vorhandene überspringen", "Skip existing": "Vorhandene überspringen",
Overwrite: "Überschreiben", Overwrite: "Überschreiben",
Options: "Optionen", Options: "Optionen",
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.", confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import-Option ausgewählt ist.",
"Keep both": "Beide behalten", "Keep both": "Beide behalten",
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert", twoFAVerifyLabel: "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert",
"Verify Token": "Token verifizieren", "Verify Token": "Token verifizieren",
"Setup 2FA": "2FA Einrichten", "Setup 2FA": "2FA einrichten",
"Enable 2FA": "2FA Aktivieren", "Enable 2FA": "2FA aktivieren",
"Disable 2FA": "2FA deaktivieren", "Disable 2FA": "2FA deaktivieren",
"2FA Settings": "2FA Einstellungen", "2FA Settings": "2FA-Einstellungen",
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?", confirmEnableTwoFAMsg: "Bist du sicher, dass du 2FA aktivieren möchtest?",
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?", confirmDisableTwoFAMsg: "Bist du sicher, dass du 2FA deaktivieren möchtest?",
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.", tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA-Einstellungen speichern.",
"Two Factor Authentication": "Zwei Faktor Authentifizierung", "Two Factor Authentication": "Zwei-Faktor-Authentifizierung",
Active: "Aktiv", Active: "Aktiv",
Inactive: "Inaktiv", Inactive: "Inaktiv",
Token: "Token", Token: "Token",
"Show URI": "URI Anzeigen", "Show URI": "URI anzeigen",
Tags: "Tags", Tags: "Tags",
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...", "Add New below or Select...": "Bestehenden Tag auswählen oder neuen hinzufügen...",
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.", "Tag with this name already exist.": "Ein Tag mit diesem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.", "Tag with this value already exist.": "Ein Tag mit diesem Wert existiert bereits.",
color: "Farbe", color: "Farbe",
"value (optional)": "Wert (Optional)", "value (optional)": "Wert (optional)",
Gray: "Grau", Gray: "Grau",
Red: "Rot", Red: "Rot",
Orange: "Orange", Orange: "Orange",
@ -162,20 +162,39 @@ export default {
Purple: "Lila", Purple: "Lila",
Pink: "Pink", Pink: "Pink",
"Search...": "Suchen...", "Search...": "Suchen...",
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall", "Heartbeat Retry Interval": "Heartbeat-Wiederholungsintervall",
retryCheckEverySecond: "Versuche alle {0} Sekunden", retryCheckEverySecond: "Versuche alle {0} Sekunden",
"Import Backup": "Import Backup", "Import Backup": "Backup importieren",
"Export Backup": "Export Backup", "Export Backup": "Backup exportieren",
"Avg. Ping": "Durchsch. Ping", "Avg. Ping": "Durchschn. Ping",
"Avg. Response": "Durchsch. Antwort", "Avg. Response": "Durchschn. Antwort",
"Entry Page": "Einstiegsseite", "Entry Page": "Einstiegsseite",
"statusPageNothing": "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.", statusPageNothing: "Noch ist hier nichts. Bitte füge eine Gruppe oder einen Monitor hinzu.",
"No Services": "Keine Dienste", "No Services": "Keine Dienste",
"All Systems Operational": "Alle Systeme Betriebsbereit", "All Systems Operational": "Alle Systeme betriebsbereit",
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst", "Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
"Degraded Service": "Eingeschränkter Dienst", "Degraded Service": "Eingeschränkter Dienst",
"Add Group": "Gruppe hinzufügen", "Add Group": "Gruppe hinzufügen",
"Add a monitor": "Monitor hinzufügen", "Add a monitor": "Monitor hinzufügen",
"Edit Status Page": "Bearbeite Statusseite", "Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard", "Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite",
telegram: "Telegram",
webhook: "Webhook",
smtp: "E-Mail (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Unterstützung für 50+ Benachrichtigungsdienste)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
}; };

View file

@ -19,7 +19,7 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", 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 you want to delete ALL statistics?",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", 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.", 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",
@ -33,6 +33,7 @@ export default {
Appearance: "Appearance", Appearance: "Appearance",
Theme: "Theme", Theme: "Theme",
General: "General", General: "General",
"Primary Base URL": "Primary Base URL",
Version: "Version", Version: "Version",
"Check Update On GitHub": "Check Update On GitHub", "Check Update On GitHub": "Check Update On GitHub",
List: "List", List: "List",
@ -75,6 +76,9 @@ export default {
"Upside Down Mode": "Upside Down Mode", "Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects", "Max. Redirects": "Max. Redirects",
"Accepted Status Codes": "Accepted Status Codes", "Accepted Status Codes": "Accepted Status Codes",
"Push URL": "Push URL",
needPushEvery: "You should call this url every {0} seconds.",
pushOptionalParams: "Optional parameters: {0}",
Save: "Save", Save: "Save",
Notifications: "Notifications", Notifications: "Notifications",
"Not available, please setup.": "Not available, please setup.", "Not available, please setup.": "Not available, please setup.",
@ -169,7 +173,7 @@ export default {
"Avg. Ping": "Avg. Ping", "Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response", "Avg. Response": "Avg. Response",
"Entry Page": "Entry Page", "Entry Page": "Entry Page",
"statusPageNothing": "Nothing here, please add a group or a monitor.", statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services", "No Services": "No Services",
"All Systems Operational": "All Systems Operational", "All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service", "Partially Degraded Service": "Partially Degraded Service",
@ -178,5 +182,127 @@ export default {
"Add a monitor": "Add a monitor", "Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
// Start notification form
defaultNotificationName: "My {notification} Alert ({number})",
here: "here",
"Required": "Required",
"telegram": "Telegram",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "You can get a token from {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "You can get your chat id by sending message to the bot and go to this url to view the chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
chatIDNotFound: "Chat ID is not found, please send a message to this bot first",
"webhook": "Webhook",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} is good for any modern http servers such as express.js",
webhookFormDataDesc: "{multipart} is good for PHP, you just need to parse the json by {decodeFunction}",
"smtp": "Email (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
"From Email": "From Email",
emailCustomSubject: "Custom Subject",
"To Email": "To Email",
smtpCC: "CC",
smtpBCC: "BCC",
"discord": "Discord",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "You can get this by going to Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Bot Display Name",
"Prefix Custom Message": "Prefix Custom Message",
"Hello @everyone is...": "Hello {'@'}everyone is...",
"teams": "Microsoft Teams",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "You can learn how to create a webhook url {0}.",
"signal": "Signal",
"Number": "Number",
"Recipients": "Recipients",
needSignalAPI: "You need to have a signal client with REST API.",
wayToCheckSignalURL: "You can check this url to view how to setup one:",
signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!",
"gotify": "Gotify",
"Application Token": "Application Token",
"Server URL": "Server URL",
"Priority": "Priority",
"slack": "Slack",
"Icon Emoji": "Icon Emoji",
"Channel Name": "Channel Name",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "More info about webhooks on: {0}",
aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the webhook channel. Ex: #other-channel",
aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.",
emojiCheatSheet: "Emoji cheat sheet: {0}",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"User Key": "User Key",
"Device": "Device",
"Message Title": "Message Title",
"Notification Sound": "Notification Sound",
"More info on:": "More info on: {0}",
pushoverDesc1: "Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.",
pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.",
"SMS Type": "SMS Type",
octopushTypePremium: "Premium (Fast - recommended for alerting)",
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)",
checkPrice: "Check {0} prices:",
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
"Check octopush prices": "Check octopush prices {0}.",
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Device ID",
"Apprise URL": "Apprise URL",
"Example:": "Example: {0}",
"Read more:": "Read more: {0}",
"Status:": "Status: {0}",
"Read more": "Read more",
appriseInstalled: "Apprise is installed.",
appriseNotInstalled: "Apprise is not installed. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.",
"Icon URL": "Icon URL",
aboutIconURL: "You can provide a link to a picture in \"Icon URL\" to override the default profile picture. Will not be used if Icon Emoji is set.",
aboutMattermostChannelName: "You can override the default channel that webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel",
"matrix": "Matrix",
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use Your Sender Name (You need to register name first). Reliable for alerts.",
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookUrl",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room Id",
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
// End notification form
Method: "Method",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers are not valid JSON: ",
BodyInvalidFormat: "The request body is not valid JSON: ",
"Monitor History": "Monitor History:",
clearDataOlderThan: "Keep monitor history data for {0} days.",
records: "records",
"One record": "One record",
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records",
steamApiKeyDescription: "For monitoring a Steam Gameserver you need a steam Web-API key. You can register your api key here: https://steamcommunity.com/dev", steamApiKeyDescription: "For monitoring a Steam Gameserver you need a steam Web-API key. You can register your api key here: https://steamcommunity.com/dev",
}; };

View file

@ -9,7 +9,7 @@ export default {
passwordNotMatchMsg: "La contraseña repetida no coincide.", passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).", notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas", keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
pauseDashboardHome: "Pausar", pauseDashboardHome: "Pausado",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?", deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?", deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.", resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
@ -17,7 +17,7 @@ export default {
pauseMonitorMsg: "¿Seguro que quieres pausar?", pauseMonitorMsg: "¿Seguro que quieres pausar?",
Settings: "Ajustes", Settings: "Ajustes",
Dashboard: "Panel", Dashboard: "Panel",
"New Update": "Vueva actualización", "New Update": "Nueva actualización",
Language: "Idioma", Language: "Idioma",
Appearance: "Apariencia", Appearance: "Apariencia",
Theme: "Tema", Theme: "Tema",
@ -32,7 +32,7 @@ export default {
Down: "Caído", Down: "Caído",
Pending: "Pendiente", Pending: "Pendiente",
Unknown: "Desconocido", Unknown: "Desconocido",
Pause: "Pausa", Pause: "Pausar",
Name: "Nombre", Name: "Nombre",
Status: "Estado", Status: "Estado",
DateTime: "Fecha y Hora", DateTime: "Fecha y Hora",
@ -61,11 +61,11 @@ export default {
Retries: "Reintentos", Retries: "Reintentos",
Advanced: "Avanzado", Advanced: "Avanzado",
"Upside Down Mode": "Modo invertido", "Upside Down Mode": "Modo invertido",
"Max. Redirects": "Máx. redirecciones", "Max. Redirects": "Redirecciones Máximas",
"Accepted Status Codes": "Códigos de estado aceptados", "Accepted Status Codes": "Códigos de estado aceptados",
Save: "Guardar", Save: "Guardar",
Notifications: "Notificaciones", Notifications: "Notificaciones",
"Not available, please setup.": "No disponible, por favor configurar.", "Not available, please setup.": "No disponible, por favor configúrelo.",
"Setup Notification": "Configurar notificación", "Setup Notification": "Configurar notificación",
Light: "Claro", Light: "Claro",
Dark: "Oscuro", Dark: "Oscuro",
@ -87,7 +87,7 @@ export default {
"Enable Auth": "Habilitar Autenticación", "Enable Auth": "Habilitar Autenticación",
Logout: "Cerrar sesión", Logout: "Cerrar sesión",
Leave: "Salir", Leave: "Salir",
"I understand, please disable": "Lo comprendo, por favor deshabilitar", "I understand, please disable": "Entiendo, por favor deshabilitar",
Confirm: "Confirmar", Confirm: "Confirmar",
Yes: "Sí", Yes: "Sí",
No: "No", No: "No",
@ -108,75 +108,99 @@ export default {
"Repeat Password": "Repetir contraseña", "Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)", respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
Create: "Create", Create: "Crear",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "¿Está seguro de que desea eliminar todos los eventos de este monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "¿Está seguro de que desea eliminar todos los latidos de este monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "¿Está seguro de que desea eliminar TODAS las estadísticas?",
"Clear Data": "Clear Data", "Clear Data": "Borrar Datos",
Events: "Events", Events: "Eventos",
Heartbeats: "Heartbeats", Heartbeats: "Latidos",
"Auto Get": "Auto Get", "Auto Get": "Obtener automáticamente",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puede deshabilitar la notificación por separado para cada monitor.",
"Default enabled": "Default enabled", "Default enabled": "Habilitado por defecto",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "También se aplica a monitores existentes",
Export: "Export", Export: "Exportar",
Import: "Import", Import: "Importar",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "Puede hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.",
backupDescription2: "PS: History and event data is not included.", backupDescription2: "PD: el historial y los datos de eventos no están incluidos.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdelo con cuidado.",
alertNoFile: "Please select a file to import.", alertNoFile: "Seleccione un archivo para importar.",
alertWrongFileType: "Please select a JSON file.", alertWrongFileType: "Seleccione un archivo JSON.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", twoFAVerifyLabel: "Ingrese su token para verificar que 2FA está funcionando",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", tokenValidSettingsMsg: "¡El token es válido! Ahora puede guardar la configuración de 2FA.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmEnableTwoFAMsg: "¿Estás seguro de que quieres habilitar 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", confirmDisableTwoFAMsg: "¿Estás seguro de que quieres desactivar 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors", "Apply on all existing monitors": "Aplicar en todos los monitores existentes",
"Verify Token": "Verify Token", "Verify Token": "Verificar token",
"Setup 2FA": "Setup 2FA", "Setup 2FA": "Configurar 2FA",
"Enable 2FA": "Enable 2FA", "Enable 2FA": "Habilitar 2FA",
"Disable 2FA": "Disable 2FA", "Disable 2FA": "Desactivar 2FA",
"2FA Settings": "2FA Settings", "2FA Settings": "Ajustes 2FA",
"Two Factor Authentication": "Two Factor Authentication", "Two Factor Authentication": "Autenticación de dos factores",
Active: "Active", Active: "Activo",
Inactive: "Inactive", Inactive: "Inactivo",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Mostrar URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Borrar todas las estadísticas",
retryCheckEverySecond: "Retry every {0} seconds.", retryCheckEverySecond: "Reintentar cada {0} segundo.",
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.", importHandleDescription: "Elija 'Omitir existente' si desea omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrese de haber seleccionado la opción de importación correcta.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Heartbeat Retry Interval": "Intervalo de reintento de latido",
"Import Backup": "Import Backup", "Import Backup": "Importar copia de seguridad",
"Export Backup": "Export Backup", "Export Backup": "Exportar copia de seguridad",
"Skip existing": "Skip existing", "Skip existing": "Omitir existente",
Overwrite: "Overwrite", Overwrite: "Sobrescribir",
Options: "Options", Options: "Opciones",
"Keep both": "Keep both", "Keep both": "Mantén ambos",
Tags: "Tags", Tags: "Etiquetas",
"Add New below or Select...": "Add New below or Select...", "Add New below or Select...": "Agregar nuevo a continuación o Seleccionar...",
"Tag with this name already exist.": "Tag with this name already exist.", "Tag with this name already exist.": "La etiqueta con este nombre ya existe.",
"Tag with this value already exist.": "Tag with this value already exist.", "Tag with this value already exist.": "La etiqueta con este valor ya existe.",
color: "color", color: "color",
"value (optional)": "value (optional)", "value (optional)": "valor (opcional)",
Gray: "Gray", Gray: "Gris",
Red: "Red", Red: "Rojo",
Orange: "Orange", Orange: "Naranja",
Green: "Green", Green: "Verde",
Blue: "Blue", Blue: "Azul",
Indigo: "Indigo", Indigo: "Índigo",
Purple: "Purple", Purple: "Morado",
Pink: "Pink", Pink: "Rosa",
"Search...": "Search...", "Search...": "Buscar...",
"Avg. Ping": "Avg. Ping", "Avg. Ping": "Ping promedio",
"Avg. Response": "Avg. Response", "Avg. Response": "Respuesta promedio",
"Entry Page": "Entry Page", "Entry Page": "Página de entrada",
"statusPageNothing": "Nothing here, please add a group or a monitor.", statusPageNothing: "No hay nada aquí, agregue un grupo o un monitor.",
"No Services": "No Services", "No Services": "Sin servicio",
"All Systems Operational": "All Systems Operational", "All Systems Operational": "Todos los sistemas están operativos",
"Partially Degraded Service": "Partially Degraded Service", "Partially Degraded Service": "Servicio parcialmente degradado",
"Degraded Service": "Degraded Service", "Degraded Service": "Servicio degradado",
"Add Group": "Add Group", "Add Group": "Agregar Grupo",
"Add a monitor": "Add a monitor", "Add a monitor": "Agregar un monitor",
"Edit Status Page": "Edit Status Page", "Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Go to Dashboard", "Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Admite más de 50 servicios de notificación)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Monitor History": "Historial de monitor:",
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
records: "registros",
"One record": "Un registro",
"Showing {from} to {to} of {count} records": "Mostrando desde {from} a {to} de {count} registros",
}; };

View file

@ -1,21 +1,22 @@
export default { export default {
languageName: "eesti", languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.", retryCheckEverySecond: "Kontrolli {0} sekundilise vahega.",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.", 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",
Dashboard: "Töölaud", Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.", "New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel", Language: "Keel",
@ -26,20 +27,21 @@ 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",
@ -49,7 +51,7 @@ export default {
"-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,14 +110,14 @@ export default {
"Repeat Password": "korda salasõna", "Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)", respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.", enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusteenuset. Seiretelt võib teavitusteenuse ühekaupa eemaldada.",
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?", 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?",
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",
@ -123,60 +125,76 @@ export default {
"Auto Get": "Hangi automaatselt", "Auto Get": "Hangi automaatselt",
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",
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.",
retryCheckEverySecond: "Retry every {0} seconds.", confirmImportMsg: "Käkerdistest hoidumiseks lae enne taastamist alla uus varukoopia. Kas soovid taastada üles laetud?",
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.", "Heartbeat Retry Interval": "Korduskatsete intervall",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", "Import Backup": "Varukoopia importimine",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Export Backup": "Varukoopia eksportimine",
"Import Backup": "Import Backup", "Skip existing": "lisa praegustele",
"Export Backup": "Export Backup", Overwrite: "asenda",
"Skip existing": "Skip existing", Options: "Mestimisviis",
Overwrite: "Overwrite", "Keep both": "kombineeri",
Options: "Options", Tags: "Sildid",
"Keep both": "Keep both", "Add New below or Select...": "Leia või lisa all uus…",
Tags: "Tags", "Tag with this name already exist.": "Selle nimega silt on juba olemas.",
"Add New below or Select...": "Add New below or Select...", "Tag with this value already exist.": "Selle väärtusega silt on juba olemas.",
"Tag with this name already exist.": "Tag with this name already exist.", color: "värvus",
"Tag with this value already exist.": "Tag with this value already exist.", "value (optional)": "väärtus (fakultatiivne)",
color: "color", Gray: "hall",
"value (optional)": "value (optional)", Red: "punane",
Gray: "Gray", Orange: "oranž",
Red: "Red", Green: "roheline",
Orange: "Orange", Blue: "sinine",
Green: "Green", Indigo: "indigo",
Blue: "Blue", Purple: "lilla",
Indigo: "Indigo", Pink: "roosa",
Purple: "Purple", "Search...": "Otsi…",
Pink: "Pink", "Avg. Ping": "Keskmine ping",
"Search...": "Search...", "Avg. Response": "Keskmine reaktsiooniaeg",
"Avg. Ping": "Avg. Ping", "Entry Page": "Avaleht",
"Avg. Response": "Avg. Response", statusPageNothing: "Kippu ega kõppu; siia saab lisada seireid või -gruppe.",
"Entry Page": "Entry Page", "No Services": "Teenused puuduvad.",
"statusPageNothing": "Nothing here, please add a group or a monitor.", "All Systems Operational": "Kõik töökorras",
"No Services": "No Services", "Partially Degraded Service": "Teenuse töö osaliselt häiritud",
"All Systems Operational": "All Systems Operational", "Degraded Service": "Teenuse töö häiritud",
"Partially Degraded Service": "Partially Degraded Service", "Add Group": "Lisa grupp",
"Degraded Service": "Degraded Service", "Edit Status Page": "Muuda lehte",
"Add Group": "Add Group", "Go to Dashboard": "Töölauale",
"Add a monitor": "Add a monitor", checkEverySecond: "Kontrolli peale tõrget {0} sekundilise vahega.",
"Edit Status Page": "Edit Status Page", telegram: "Telegram",
"Go to Dashboard": "Go to Dashboard", webhook: "Webhook",
smtp: "elektronpost (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (vahendab üle 65 teavitusteenust)",
pushbullet: "Pushbullet",
line: "LINE",
mattermost: "Mattermost",
}; };

Some files were not shown because too many files have changed in this diff Show more