Merge branch 'master' into fix_pl_i18n

This commit is contained in:
Adam Stachowicz 2021-10-04 23:29:21 +02:00
commit 23e64b8efd
69 changed files with 8865 additions and 2693 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

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,
},
} }
] ]
}; };

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ dist-ssr
/private /private
/out /out
/tmp

View File

@ -4,11 +4,30 @@ 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.
# Key Technical Skills
- Node.js (You should know what are promise, async/await and arrow function etc.)
- Socket.io
- SCSS
- Vue.js
- Bootstrap
- SQLite
# Directories
- data (App data)
- dist (Frontend build)
- extra (Extra useful scripts)
- public (Frontend resources for dev only)
- server (Server source code)
- src (Frontend source code)
- test (unit test)
# Can I create a pull request for Uptime Kuma? # Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested. Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested.
If you are not sure, feel free to create an empty pull request draft first. If you are not sure, feel free to create an empty pull request draft first.
@ -43,15 +62,14 @@ It changed my current workflow and require further studies.
I personally do not like something need to learn so much and need to config so much before you can finally start the app. I personally do not like something need to learn so much and need to config so much before you can finally start the app.
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run - Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go - Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
- All settings in frontend. - Settings should be configurable in the frontend. Env var is not encouraged.
- Easy to use - Easy to use
# Coding Styles # Coding Styles
- 4 spaces indentation
- Follow `.editorconfig` - Follow `.editorconfig`
- Follow ESLint - Follow ESLint
@ -65,22 +83,16 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- 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)
@ -96,28 +108,24 @@ 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".
1. Start the frontend dev server by the following command.
```bash ```bash
npm run dev npm run dev
``` ```
It binds to `0.0.0.0:3000` by default.
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
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:
```javascript
localStorage.dev = "dev";
```
So that the frontend will try to connect websocket server in 3001.
Alternately, you can specific `NODE_ENV` to "development".
## Build the frontend ## Build the frontend
```bash ```bash
@ -134,10 +142,11 @@ 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

View File

@ -1,6 +1,7 @@
# Uptime Kuma # Uptime Kuma
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Backers&color=brightgreen" /></a>
<div align="center" width="100%"> <div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" /> <img src="./public/icon.svg" width="128" alt="" />

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/*

View File

@ -1,6 +1,4 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372. FROM louislam/uptime-kuma:base-debian AS build
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim AS build
WORKDIR /app WORKDIR /app
COPY . . COPY . .
@ -10,24 +8,42 @@ RUN npm install --legacy-peer-deps && \
chmod +x /app/extra/entrypoint.sh chmod +x /app/extra/entrypoint.sh
FROM node:14-buster-slim AS release FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, 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 app files from build layer
COPY --from=build /app /app COPY --from=build /app /app
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"] ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"] CMD ["node", "server/server.js"]
FROM release AS nightly FROM release AS nightly
RUN npm run mark-as-nightly RUN npm run mark-as-nightly
# Upload the artifact to Github
FROM louislam/uptime-kuma:base-debian AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file
ARG GITHUB_TOKEN
ARG TARGETARCH
ARG PLATFORM=debian
ARG VERSION
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
ARG DIST=dist.tar.gz
COPY --from=build /app /app
RUN chmod +x /app/extra/upload-github-release-asset.sh
# Full Build
# RUN tar -zcvf $FILE app
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE
# Dist only
RUN cd /app && tar -zcvf $DIST dist
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$DIST

View File

@ -1,5 +1,4 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41. FROM louislam/uptime-kuma:base-alpine AS build
FROM node:14-alpine3.12 AS build
WORKDIR /app WORKDIR /app
COPY . . COPY . .
@ -9,21 +8,16 @@ RUN npm install --legacy-peer-deps && \
chmod +x /app/extra/entrypoint.sh chmod +x /app/extra/entrypoint.sh
FROM node:14-alpine3.12 AS release FROM louislam/uptime-kuma:base-alpine AS release
WORKDIR /app WORKDIR /app
# Install apprise, 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 app files from build layer
COPY --from=build /app /app COPY --from=build /app /app
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"] ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"] CMD ["node", "server/server.js"]
FROM release AS nightly FROM release AS nightly

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

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

5
jest-puppeteer.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
"launch": {
"headless": false
}
};

8820
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.7.3",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -20,15 +20,20 @@
"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",
"prepare-test": "npm run build && node server/server.js --port=3002 --data-dir=./data/test/",
"test": "jest",
"tsc": "tsc", "tsc": "tsc",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host",
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.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-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.3-alpine --target release . --push",
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.3-debian --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.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.7.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
"update-version": "node extra/update-version.js", "update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
@ -43,66 +48,80 @@
"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": "~5.0.6",
"@popperjs/core": "^2.10.1", "@popperjs/core": "~2.10.1",
"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", "chart.js": "~3.5.1",
"chartjs-adapter-dayjs": "^1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"command-exists": "^1.2.9", "command-exists": "~1.2.9",
"compare-versions": "^3.6.0", "compare-versions": "~3.6.0",
"dayjs": "^1.10.7", "dayjs": "~1.10.7",
"express": "^4.17.1", "express": "~4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "~1.2.0",
"form-data": "^4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "^3.1.4", "http-graceful-shutdown": "~3.1.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "~8.5.1",
"nodemailer": "^6.6.5", "nodemailer": "~6.6.5",
"notp": "^2.0.3", "notp": "~2.0.3",
"password-hash": "^1.2.2", "password-hash": "~1.2.2",
"prom-client": "^13.2.0", "postcss-rtlcss": "~3.4.1",
"prometheus-api-metrics": "^3.2.0", "postcss-scss": "~4.0.0",
"qrcode": "^1.4.4", "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", "tcp-ping": "~0.1.1",
"thirty-two": "^1.0.2", "thirty-two": "~1.0.2",
"timezones-list": "^3.0.1", "timezones-list": "~3.0.1",
"v-pagination-3": "^0.1.6", "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.7",
"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", "@types/bootstrap": "~5.1.6",
"@vitejs/plugin-legacy": "^1.5.3", "@vitejs/plugin-legacy": "~1.5.3",
"@vitejs/plugin-vue": "^1.9.1", "@vitejs/plugin-vue": "~1.9.1",
"@vue/compiler-sfc": "^3.2.16", "@vue/compiler-sfc": "~3.2.16",
"core-js": "^3.18.0", "core-js": "~3.18.0",
"cross-env": "^7.0.3", "cross-env": "~7.0.3",
"dns2": "^2.0.1", "dns2": "~2.0.1",
"eslint": "^7.32.0", "eslint": "~7.32.0",
"eslint-plugin-vue": "^7.18.0", "eslint-plugin-vue": "~7.18.0",
"sass": "^1.42.1", "jest": "~27.2.4",
"stylelint": "^13.13.1", "jest-puppeteer": "~6.0.0",
"stylelint-config-standard": "^22.0.0", "puppeteer": "~10.4.0",
"typescript": "^4.4.3", "sass": "~1.42.1",
"vite": "^2.5.10" "stylelint": "~13.13.1",
"stylelint-config-standard": "~22.0.0",
"typescript": "~4.4.3",
"vite": "~2.5.10"
},
"jest": {
"verbose": true,
"preset": "jest-puppeteer",
"globals": {
"__DEV__": true
},
"testRegex": "./test/*.spec.js",
"rootDir": "."
} }
} }

View File

@ -22,7 +22,6 @@ exports.startInterval = () => {
} }
exports.latestVersion = res.data.version; exports.latestVersion = res.data.version;
console.log("Latest Version: " + exports.latestVersion);
} catch (_) { } } catch (_) { }
}; };

View File

@ -48,6 +48,7 @@ class Database {
"patch-add-retry-interval-monitor.sql": true, "patch-add-retry-interval-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,
} }
/** /**
@ -107,6 +108,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");

View File

@ -69,6 +69,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,
}; };
@ -236,6 +237,28 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage; bean.msg = dnsMessage;
bean.status = UP; bean.status = UP;
} else if (this.type === "push") { // Type: Push
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
this.id,
time
]);
debug("heartbeatCount" + heartbeatCount + " " + time);
if (heartbeatCount <= 0) {
throw new Error("No heartbeat in the time window");
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
return;
}
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
} }
if (this.isUpsideDown()) { if (this.isUpsideDown()) {
@ -263,6 +286,8 @@ class Monitor extends BeanModel {
} }
} }
let beatInterval = this.interval;
// * ? -> ANY STATUS = important [isFirstBeat] // * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important // UP -> PENDING = not important
// * UP -> DOWN = important // * UP -> DOWN = important
@ -312,12 +337,10 @@ class Monitor extends BeanModel {
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}`);
@ -339,7 +362,14 @@ class Monitor extends BeanModel {
}; };
// Delay Push Type
if (this.type === "push") {
setTimeout(() => {
beat(); beat();
}, this.interval * 1000);
} else {
beat();
}
} }
stop() { stop() {

View File

@ -4,15 +4,55 @@ 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 } = 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;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken
]);
if (! monitor) {
throw new Error("Monitor not found or not active.");
}
let bean = R.dispense("heartbeat");
bean.monitor_id = monitor.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.status = UP;
bean.msg = msg;
bean.ping = ping;
await R.store(bean);
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, monitor.id, monitor.user_id);
response.json({
ok: true,
});
} catch (e) {
response.json({
ok: false,
msg: e.message
});
}
});
// Status Page Config // 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

@ -6,7 +6,7 @@ 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"); const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
console.log("Importing Node libraries"); console.log("Importing Node libraries");
const fs = require("fs"); const fs = require("fs");
@ -37,7 +37,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 } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -71,7 +71,7 @@ if (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;
@ -303,6 +303,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 = ? ", [
@ -511,6 +517,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);
@ -638,6 +645,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({

View File

@ -272,16 +272,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);

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,4 +1,5 @@
@import "vars.scss"; @import "vars.scss";
@import "multiselect.scss";
@import "node_modules/bootstrap/scss/bootstrap"; @import "node_modules/bootstrap/scss/bootstrap";
#app { #app {
@ -179,6 +180,11 @@ h2 {
border-color: $dark-border-color; border-color: $dark-border-color;
} }
.form-control:disabled, .form-control[readonly] {
background-color: #232f3b;
opacity: 1;
}
.table-hover > tbody > tr:hover { .table-hover > tbody > tr:hover {
--bs-table-accent-bg: #070a10; --bs-table-accent-bg: #070a10;
color: $dark-font-color; color: $dark-font-color;
@ -233,30 +239,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 +410,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: 50rem;
margin-bottom: 0;
padding: 6px 26px 6px 10px;
background: $primary !important;
}
.multiselect__placeholder {
font-size: 1rem;
padding-left: 6px;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
opacity: 0.67;
}
.multiselect__input,
.multiselect__single {
line-height: 14px;
margin-bottom: 0;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input,
.multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
}

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

@ -52,7 +52,7 @@ export default {
token: "", token: "",
res: null, res: null,
tokenRequired: false, tokenRequired: false,
} };
}, },
methods: { methods: {
submit() { submit() {
@ -60,21 +60,20 @@ 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) 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 +81,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

@ -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">
@ -450,18 +72,11 @@ import { Modal } from "bootstrap"
import { ucfirst } from "../util.ts" import { ucfirst } from "../util.ts"
import Confirm from "./Confirm.vue"; import Confirm from "./Confirm.vue";
import HiddenInput from "./HiddenInput.vue"; import NotificationFormList from "./notifications"
import Telegram from "./notifications/Telegram.vue";
import 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,13 +85,23 @@ export default {
model: null, model: null,
processing: false, processing: false,
id: null, id: null,
notificationTypes: Object.keys(NotificationFormList),
notification: { notification: {
name: "", name: "",
/** @type { null | keyof NotificationFormList } */
type: null, type: null,
isDefault: false, isDefault: false,
// Do not set default value here, please scroll to show() // Do not set default value here, please scroll to show()
}
}
}, },
appriseInstalled: false,
computed: {
currentForm() {
if (!this.notification.type) {
return null
}
return NotificationFormList[this.notification.type]
} }
}, },
@ -497,10 +122,6 @@ export default {
}, },
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal)
this.$root.getSocket().emit("checkApprise", (installed) => {
this.appriseInstalled = installed;
})
}, },
methods: { methods: {
@ -528,9 +149,7 @@ export default {
} }
// Set Default value here // Set Default value here
this.notification.type = "telegram"; this.notification.type = this.notificationTypes[0];
this.notification.gotifyPriority = 8;
this.notification.smtpSecure = false;
} }
this.modal.show() this.modal.show()

View File

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

View File

@ -0,0 +1,20 @@
<template>
<div class="mb-3">
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
<div class="form-text">
You can get this by going to Server Settings -> Integrations -> Create Webhook
</div>
</div>
<div class="mb-3">
<label for="discord-username" class="form-label">Bot Display Name</label>
<input id="discord-username" v-model="$parent.notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div>
<div class="mb-3">
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
</div>
</template>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">Username</label>
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
<label for="mattermost-channel" class="form-label">Channel Name</label>
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</p>
<p style="margin-top: 8px;">
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
</p>
</div>
</div>
</template>

View File

@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">SMS Type</label>
<select id="octopush-type-sms" v-model="$parent.notification.octopushSMSType" class="form-select">
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
</select>
<div class="form-text">
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
</div>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
<input id="octopush-phone-number" v-model="$parent.notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
<input id="octopush-sender-name" v-model="$parent.notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</p>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
}
</script>

View File

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

View File

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

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>
<p style="margin-top: 8px;">
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</p>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="rocket-webhook-url" v-model="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
<label for="rocket-username" class="form-label">Username</label>
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
<label for="rocket-channel" class="form-label">Channel Name</label>
<input id="rocket-channel-name" v-model="$parent.notification.rocketchannel" type="text" class="form-control">
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
<input id="rocket-button" v-model="$parent.notification.rocketbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>

View File

@ -66,9 +66,9 @@ export default {
components: { components: {
HiddenInput, HiddenInput,
}, },
data() { mounted() {
return { if (typeof this.$parent.notification.smtpSecure === "undefined") {
name: "smtp", this.$parent.notification.smtpSecure = false;
} }
}, },
} }

View File

@ -0,0 +1,32 @@
<template>
<div class="mb-3">
<label for="signal-url" class="form-label">Post URL</label>
<input id="signal-url" v-model="$parent.notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-number" class="form-label">Number</label>
<input id="signal-number" v-model="$parent.notification.signalNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-recipients" class="form-label">Recipients</label>
<input id="signal-recipients" v-model="$parent.notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text">
You need to have a signal client with REST API.
<p style="margin-top: 8px;">
You can check this url to view how to setup one:
</p>
<p style="margin-top: 8px;">
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
</p>
<p style="margin-top: 8px;">
IMPORTANT: You cannot mix groups and numbers in recipients!
</p>
</div>
</div>
</template>

View File

@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label>
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">Channel Name</label>
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
<input id="slack-button" v-model="$parent.notification.slackbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>

View File

@ -17,13 +17,3 @@
</div> </div>
</div> </div>
</template> </template>
<script>
export default {
data() {
return {
name: "teams",
};
},
};
</script>

View File

@ -47,11 +47,6 @@ export default {
components: { components: {
HiddenInput, HiddenInput,
}, },
data() {
return {
name: "telegram",
}
},
computed: { computed: {
telegramGetUpdatesURL() { telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>" let token = "<YOUR BOT TOKEN HERE>"
@ -62,9 +57,6 @@ export default {
return `https://api.telegram.org/bot${token}/getUpdates`; return `https://api.telegram.org/bot${token}/getUpdates`;
}, },
},
mounted() {
}, },
methods: { methods: {
async autoGetTelegramChatID() { async autoGetTelegramChatID() {

View File

@ -0,0 +1,23 @@
<template>
<div class="mb-3">
<label for="webhook-url" class="form-label">Post URL</label>
<input id="webhook-url" v-model="$parent.notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="webhook-content-type" class="form-label">Content Type</label>
<select id="webhook-content-type" v-model="$parent.notification.webhookContentType" class="form-select" required>
<option value="json">
application/json
</option>
<option value="form-data">
multipart/form-data
</option>
</select>
<div class="form-text">
<p>"application/json" is good for any modern http servers such as express.js</p>
<p>"multipart/form-data" is good for PHP, you just need to parse the json by <strong>json_decode($_POST['data'])</strong></p>
</div>
</div>
</template>

View File

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

View File

@ -1,10 +1,13 @@
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import bgBG from "./languages/bg-BG";
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 fa from "./languages/fa";
import esEs from "./languages/es-ES"; import esEs from "./languages/es-ES";
import etEE from "./languages/et-EE"; import etEE from "./languages/et-EE";
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 ja from "./languages/ja"; import ja from "./languages/ja";
import koKR from "./languages/ko-KR"; import koKR from "./languages/ko-KR";
@ -22,11 +25,14 @@ 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,
"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,
"ja": ja, "ja": ja,
"da-DK": daDK, "da-DK": daDK,
@ -41,8 +47,15 @@ const languageList = {
"et-EE": etEE, "et-EE": etEE,
}; };
const rtlLangs = ["fa"];
export const currentLocale = () => localStorage.locale || "en";
export const localeDirection = () => {
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr"
}
export const i18n = createI18n({ export const i18n = createI18n({
locale: localStorage.locale || "en", locale: currentLocale(),
fallbackLocale: "en", fallbackLocale: "en",
silentFallbackWarn: true, silentFallbackWarn: true,
silentTranslationWarn: process.env.NODE_ENV !== "development", silentTranslationWarn: process.env.NODE_ENV !== "development",

View File

@ -26,7 +26,10 @@ import {
faArrowsAltV, faArrowsAltV,
faUnlink, faUnlink,
faQuestionCircle, faQuestionCircle,
faImages, faUpload, faImages,
faUpload,
faCopy,
faCheck,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -54,6 +57,8 @@ library.add(
faQuestionCircle, faQuestionCircle,
faImages, faImages,
faUpload, faUpload,
faCopy,
faCheck,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

181
src/languages/bg-BG.js Normal file
View File

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

View File

@ -178,5 +178,22 @@ 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",
"telegram": "Telegram",
"webhook": "Webhook",
"smtp": "Email (SMTP)",
"discord": "Discord",
"teams": "Microsoft Teams",
"signal": "Signal",
"gotify": "Gotify",
"slack": "Slack",
"rocket.chat": "Rocket.chat",
"pushover": "Pushover",
"pushy": "Pushy",
"octopush": "Octopush",
"lunasea": "LunaSea",
"apprise": "Apprise (Support 50+ Notification services)",
"pushbullet": "Pushbullet",
"line": "Line Messenger",
"mattermost": "Mattermost",
"Status Page": "Status Page", "Status Page": "Status Page",
}; };

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", // hääletuse tulemus, teine: seisundileht, kolmas: Olukord/Olek
Dashboard: "Töölaud", Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.", "New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel", Language: "Keel",
@ -26,30 +27,31 @@ export default {
"Check Update On GitHub": "Otsi uuendusi GitHub'ist", "Check Update On GitHub": "Otsi uuendusi GitHub'ist",
List: "Nimekiri", List: "Nimekiri",
Add: "Lisa", Add: "Lisa",
"Add New Monitor": "Seire lisamine", "Add New Monitor": "Lisa seire",
"Add a monitor": "Lisa seire",
"Quick Stats": "Ülevaade", "Quick Stats": "Ülevaade",
Up: "Töökorras", Up: "Töökorras",
Down: "Rikkis", Down: "Rikkis",
Pending: "Määramisel", Pending: "Määramisel",
Unknown: "Teadmata", Unknown: "Kahtlast",
Pause: "Seiskamine", Pause: "Seiska",
Name: "Nimi", Name: "Nimi",
Status: "Olek", Status: "Olek",
DateTime: "Kuupäev", DateTime: "Kuupäev",
Message: "Tulemus", Message: "Tulemus",
"No important events": "Märkimisväärsed juhtumid puuduvad.", "No important events": "Märkimisväärsed juhtumid puuduvad.",
Resume: "Taasta", Resume: "Taasta",
Edit: "Muutmine", Edit: "Muuda",
Delete: "Eemalda", Delete: "Eemalda",
Current: "Hetkeseisund", Current: "Hetkeseisund",
Uptime: "Eluiga", Uptime: "Eluiga", // todo: launchpad?
"Cert Exp.": "Sert. aegumine", "Cert Exp.": "Sert. aegumine",
days: "päeva", days: "päeva",
day: "päev", day: "päev",
"-day": "-päev", "-day": "-päev",
hour: "tund", hour: "tund",
"-hour": "-tund", "-hour": "-tund",
Response: "Vastus", Response: "Reaktsiooniaeg",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Seire tüüp", "Monitor Type": "Seire tüüp",
Keyword: "Võtmesõna", Keyword: "Võtmesõna",
@ -107,77 +109,74 @@ export default {
"Create your admin account": "Admininstraatori konto loomine", "Create your admin account": "Admininstraatori konto loomine",
"Repeat Password": "korda salasõna", "Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)", respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A", // tõlkimata, umbkaudu rahvusvaheline termin peaks sobima piisavalt
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.", enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusteenuset. Seiretelt võib teavitusteenuse ühekaupa eemaldada.",
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?", clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?", clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?", confirmClearStatisticsMsg: "Kas soovid TERVE ajaloo kustutada?",
Export: "Eksport", Export: "Eksport",
Import: "Import", Import: "Import",
"Default enabled": "Kasuta vaikimisi", "Default enabled": "Kasuta vaikimisi",
"Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel", "Apply on all existing monitors": "Kõik praegused seired hakkavad kasutama seda teavitusteenust",
Create: "Loo konto", Create: "Loo konto",
"Clear Data": "Eemalda andmed", "Clear Data": "Eemalda andmed",
Events: "Sündmused", Events: "Sündmused",
Heartbeats: "Tuksed", Heartbeats: "Tuksed",
"Auto Get": "Hangi automaatselt", "Auto Get": "Hangi automaatselt", // hangi? kõlab liiga otsetõlge
backupDescription: "Varunda kõik seired ja teavitused JSON faili.", backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.", backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.", backupDescription3: "Varukoopiad sisaldavad teavitusteenusete pääsuvõtmeid.",
alertNoFile: "Palun lisa fail, mida importida.", alertNoFile: "Palun lisa fail, mida importida.",
alertWrongFileType: "Palun lisa JSON-formaadis fail.", alertWrongFileType: "Palun lisa JSON-formaadis fail.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", twoFAVerifyLabel: "2FA kinnitamiseks sisesta pääsukood",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", tokenValidSettingsMsg: "Kood õige. Akna võib sulgeda.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmEnableTwoFAMsg: "Kas soovid 2FA sisse lülitada?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", confirmDisableTwoFAMsg: "Kas soovid 2FA välja lülitada?",
"Apply on all existing monitors": "Apply on all existing monitors", "Verify Token": "Kontrolli",
"Verify Token": "Verify Token", "Setup 2FA": "Kaksikautentimise seadistamine",
"Setup 2FA": "Setup 2FA", "Enable 2FA": "Seadista 2FA",
"Enable 2FA": "Enable 2FA", "Disable 2FA": "Lülita 2FA välja",
"Disable 2FA": "Disable 2FA", "2FA Settings": "2FA seaded",
"2FA Settings": "2FA Settings", "Two Factor Authentication": "Kaksikautentimine",
"Two Factor Authentication": "Two Factor Authentication", Active: "kasutusel",
Active: "Active", Inactive: "seadistamata",
Inactive: "Inactive", Token: "kaksikautentimise kood", // needs to compensate for no title
Token: "Token", "Show URI": "Näita URId",
"Show URI": "Show URI", "Clear all statistics": "Tühjenda ajalugu",
"Clear all statistics": "Clear all Statistics", importHandleDescription: "'kombineeri' täiendab varukoopiast ja kirjutab üle samanimelised seireid ja teavitusteenused; 'lisa praegustele' jätab olemasolevad puutumata; 'asenda' kustutab ja asendab kõik seired ja teavitusteenused.",
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", // reusal of key would be chaos
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)", // milline sõna!
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", // pikk, aga nagunii kahel real
"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",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page", "Status Page": "Status Page",
}; };

190
src/languages/fa.js Normal file
View File

@ -0,0 +1,190 @@
export default {
languageName: "Farsi",
checkEverySecond: "بررسی هر {0} ثانیه.",
retryCheckEverySecond: "تکرار مجدد هر {0} ثانیه.",
retriesDescription: "حداکثر تعداد تکرار پیش از علامت گذاری وب‌سایت بعنوان خارج از دسترس و ارسال اطلاع‌رسانی.",
ignoreTLSError: "بی‌خیال ارور TLS/SSL برای سایت‌های HTTPS",
upsideDownModeDescription: "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است!",
maxRedirectDescription: "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ری‌دایرکت‌ها پشتیبانی نشوند، عدد 0 را وارد کنید.",
acceptedStatusCodesDescription: "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.",
passwordNotMatchMsg: "تکرار رمز عبور مطابقت ندارد!",
notificationDescription: "برای اینکه سرویس اطلاع‌رسانی کار کند، آنرا به یکی از مانیتور‌ها متصل کنید.",
keywordDescription: "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).",
pauseDashboardHome: "متوقف شده",
deleteMonitorMsg: "آیا از حذف این مانیتور مطمئن هستید؟",
deleteNotificationMsg: "آیا مطمئن هستید که میخواهید این سرویس اطلاع‌رسانی را برای تمامی مانیتورها حذف کنید؟",
resoverserverDescription: "سرویس CloudFlare به عنوان سرور پیش‌فرض استفاده می‌شود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.",
rrtypeDescription: "لطفا نوع Resource Record را انتخاب کنید.",
pauseMonitorMsg: "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟",
enableDefaultNotificationDescription: "برای هر مانیتور جدید، این سرویس اطلاع‌رسانی به صورت پیش‌فرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.",
clearEventsMsg: "آیا از اینکه تمامی تاریخچه رویداد‌های این مانیتور حذف شود مطمئن هستید؟",
clearHeartbeatsMsg: "آیا از اینکه تاریخچه تمامی Heartbeat های این مانیتور حذف شود مطمئن هستید؟ ",
confirmClearStatisticsMsg: "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟",
importHandleDescription: " اگر که میخواهید بیخیال مانیتورها و یا سرویس‌های اطلاع‌رسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بی‌خیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.",
confirmImportMsg: "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کرده‌اید اطمینان حاصل کنید!",
twoFAVerifyLabel: "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحله‌ای توکن خود را وارد کنید!",
tokenValidSettingsMsg: "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحله‌ای را فعال کنید!",
confirmEnableTwoFAMsg: " آیا از فعال سازی احراز هویت دو مرحله‌ای مطمئن هستید؟",
confirmDisableTwoFAMsg: "آیا از غیرفعال سازی احراز هویت دومرحله‌ای مطمئن هستید؟",
Settings: "تنظیمات",
Dashboard: "پیشخوان",
"New Update": "بروزرسانی جدید!",
Language: "زبان",
Appearance: "ظاهر",
Theme: "پوسته",
General: "عمومی",
Version: "نسخه",
"Check Update On GitHub": "بررسی بروزرسانی بر روی گیت‌هاب",
List: "لیست",
Add: "اضافه",
"Add New Monitor": "اضافه کردن مانیتور جدید",
"Quick Stats": "خلاصه وضعیت",
Up: "فعال",
Down: "غیرفعال",
Pending: "در انتظار تایید",
Unknown: "نامشخص",
Pause: "توقف",
Name: "نام",
Status: "وضعیت",
DateTime: "تاریخ و زمان",
Message: "پیام",
"No important events": "رخداد جدیدی موجود نیست.",
Resume: "ادامه",
Edit: "ویرایش",
Delete: "حذف",
Current: "فعلی",
Uptime: "آپتایم",
"Cert Exp.": "تاریخ انقضای SSL",
days: "روز",
day: "روز",
"-day": "-روز",
hour: "ساعت",
"-hour": "-ساعت",
Response: "پاسخ",
Ping: "Ping",
"Monitor Type": "نوع مانیتور",
Keyword: "کلمه کلیدی",
"Friendly Name": "عنوان",
URL: "آدرس (URL)",
Hostname: "نام میزبان (Hostname)",
Port: "پورت",
"Heartbeat Interval": "فاصله هر Heartbeat",
Retries: "تلاش مجدد",
"Heartbeat Retry Interval": "فاصله تلاش مجدد برایHeartbeat",
Advanced: "پیشرفته",
"Upside Down Mode": "حالت بر عکس",
"Max. Redirects": "حداکثر تعداد ری‌دایرکت",
"Accepted Status Codes": "وضعیت‌های (Status Code) های قابل قبول",
Save: "ذخیره",
Notifications: "اطلاع‌رسانی‌ها",
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید!",
"Setup Notification": "راه اندازی اطلاع‌رسانی‌",
Light: "روشن",
Dark: "تاریک",
Auto: "اتوماتیک",
"Theme - Heartbeat Bar": "ظاهر نوار Heartbeat",
Normal: "معمولی",
Bottom: "پایین",
None: "هیچ کدام",
Timezone: "موقعیت زمانی",
"Search Engine Visibility": "قابلیت دسترسی برای موتورهای جستجو",
"Allow indexing": "اجازه ایندکس شدن را بده.",
"Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده.",
"Change Password": "تغییر رمزعبور",
"Current Password": "رمزعبور فعلی",
"New Password": "رمزعبور جدید",
"Repeat New Password": "تکرار رمزعبور جدید",
"Update Password": "بروز رسانی رمز عبور",
"Disable Auth": "غیر فعال سازی تایید هویت",
"Enable Auth": "فعال سازی تایید هویت",
Logout: "خروج",
Leave: "منصرف شدم",
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!",
Confirm: "تایید",
Yes: "بلی",
No: "خیر",
Username: "نام کاربری",
Password: "کلمه عبور",
"Remember me": "مراب هب خاطر بسپار",
Login: "ورود",
"No Monitors, please": "هیچ مانیتوری موجود نیست، لطفا",
"add one": "یک مورد اضافه کنید",
"Notification Type": "نوع اطلاع‌رسانی",
Email: "ایمیل",
Test: "تست",
"Certificate Info": "اطلاعات سرتیفیکت",
"Resolver Server": "سرور Resolver",
"Resource Record Type": "نوع رکورد (Resource Record Type)",
"Last Result": "آخرین نتیجه",
"Create your admin account": "ایجاد حساب کاربری مدیر",
"Repeat Password": "تکرار رمز عبور",
"Import Backup": "بازگردانی فایل پشتیبان",
"Export Backup": "ذخیره فایل پشتیبان",
Export: "استخراج اطلاعات",
Import: "ورود اطلاعات",
respTime: "زمان پاسخگویی (میلی‌ثانیه)",
notAvailableShort: "ناموجود",
"Default enabled": "به صورت پیش‌فرض فعال باشد.",
"Apply on all existing monitors": "بر روی تمامی مانیتور‌های فعلی اعمال شود.",
Create: "ایجاد",
"Clear Data": "پاکسازی داده‌ها",
Events: "رخداد‌ها",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "شما میتوانید تمامی مانیتورها و تنظیمات اطلاع‌رسانی‌ها را در قالب یه فایل JSON دریافت کنید.",
backupDescription2: "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.",
backupDescription3: "توجه داشته باشید که تمامی اطلاعات حساس شما مانند توکن‌ها نیز در این فایل وجود خواهد داشت ، پس از این فایل به خوبی مراقبت کنید.",
alertNoFile: "لطفا یک فایل برای «ورود اطلاعات» انتخاب کنید..",
alertWrongFileType: "یک فایل JSON انتخاب کنید.",
"Clear all statistics": "پاکسازی تمامی آمار و ارقام",
"Skip existing": "بی‌خیال مواردی که از قبل موجود است",
Overwrite: "بازنویسی",
Options: "تنظیمات",
"Keep both": "هر دو را نگه‌ دار",
"Verify Token": "تایید توکن",
"Setup 2FA": "تنظیمات احراز دو مرحله‌ای",
"Enable 2FA": "فعال سازی احراز 2 مرحله‌ای",
"Disable 2FA": "غیر فعال کردن احراز 2 مرحله‌ای",
"2FA Settings": "تنظیمات احراز 2 مرحله‌ای",
"Two Factor Authentication": "احراز هویت دومرحله‌ای",
Active: "فعال",
Inactive: "غیرفعال",
Token: "توکن",
"Show URI": "نمایش آدرس (URI) ",
Tags: "برچسب‌ها",
"Add New below or Select...": "یک مورد جدید اضافه کنید و یا از لیست انتخاب کنید...",
"Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد",
"Tag with this value already exist.": "یک برچسب با این «مقدار» از قبل وجود دارد.",
color: "رنگ",
"value (optional)": "مقدار (اختیاری)",
Gray: "خاکستری",
Red: "قرمز",
Orange: "نارنجی",
Green: "سبز",
Blue: "آبی",
Indigo: "نیلی",
Purple: "بنفش",
Pink: "صورتی",
"Search...": "جستجو...",
"Avg. Ping": "متوسط پینگ",
"Avg. Response": "متوسط زمان پاسخ",
"Entry Page": "صفحه ورودی",
statusPageNothing: "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید!",
"No Services": "هیچ سرویسی موجود نیست",
"All Systems Operational": "تمامی سیستم‌ها عملیاتی هستند!",
"Partially Degraded Service": "افت نسبی کیفیت سرویس",
"Degraded Service": "افت کامل کیفیت سرویس",
"Add Group": "اضافه کردن گروه",
"Add a monitor": "اضافه کردن مانیتور",
"Edit Status Page": "ویرایش صفحه وضعیت",
"Status Page": "صفحه وضعیت",
"Go to Dashboard": "رفتن به پیشخوان",
"Uptime Kuma": "آپتایم کوما",
records: "مورد",
"One record": "یک مورد",
"Showing {from} to {to} of {count} records": "نمایش از {from} تا {to} از {count} مورد",
First: "اولین",
Last: "آخرین",
Info: "اطلاعات",
"Powered By": "نیرو گرفته از",
};

181
src/languages/hu.js Normal file
View File

@ -0,0 +1,181 @@
export default {
languageName: "Magyar",
checkEverySecond: "Ellenőrzés {0} másodpercenként",
retryCheckEverySecond: "Újrapróbál {0} másodpercenként.",
retriesDescription: "Maximális próbálkozás mielőtt a szolgáltatás leállt jelőlést kap és értesítés kerül kiküldésre",
ignoreTLSError: "TLS/SSL hibák figyelnen kívül hagyása HTTPS weboldalaknál",
upsideDownModeDescription: "Az állapot megfordítása. Ha a szolgáltatás elérhető, akkor lesz leállt állapotú.",
maxRedirectDescription: "Az átirányítások maximális száma. állítsa 0-ra az átirányítás tiltásához.",
acceptedStatusCodesDescription: "Válassza ki az állapot kódokat amelyek sikeres válasznak fognak számítani.",
passwordNotMatchMsg: "A megismételt jelszó nem egyezik.",
notificationDescription: "Kérem, rendeljen egy értesítést a figyeléshez, hogy működjön.",
keywordDescription: "Kulcsszó keresése a html-ben vagy a JSON válaszban. (kis-nagybetű érzékeny)",
pauseDashboardHome: "Szünetel",
deleteMonitorMsg: "Biztos, hogy törölni akarja ezt a figyelőt?",
deleteNotificationMsg: "Biztos, hogy törölni akarja ezt az értesítést az összes figyelőnél?",
resoverserverDescription: "A Cloudflare az alapértelmezett szerver, bármikor meg tudja változtatni a resolver server-t.",
rrtypeDescription: "Válassza ki az RR-Típust a figyelőhöz",
pauseMonitorMsg: "Biztos, hogy szüneteltetni akarja?",
enableDefaultNotificationDescription: "Minden új figyelőhöz ez az értesítés engedélyezett lesz alapértelmezetten. Kikapcsolhatja az értesítést külön minden figyelőnél.",
clearEventsMsg: "Biztos, hogy törölni akar miden eseményt ennél a figyelnél?",
clearHeartbeatsMsg: "Biztos, hogy törölni akar minden heartbeat-et ennél a figyelőnél?",
confirmClearStatisticsMsg: "Biztos, hogy törölni akat MINDEN statisztikát?",
importHandleDescription: "Válassza a 'Meglévő kihagyását', ha ki szeretné hagyni az azonos nevő figyelőket vagy értesítésket. A 'Felülírás' törölni fog minden meglévő figyelőt és értesítést.",
confirmImportMsg: "Biztos, hogy importálja a mentést? Győzödjön meg róla, hogy jól választotta ki az importálás opciót.",
twoFAVerifyLabel: "Kérem, adja meg a token-t, hogy a 2FA működését ellenőrizzük",
tokenValidSettingsMsg: "A token érvényes! El tudja menteni a 2FA beállításait.",
confirmEnableTwoFAMsg: "Biztosan engedélyezi a 2FA-t?",
confirmDisableTwoFAMsg: "Biztosan letiltja a 2FA-t?",
Settings: "Beállítások",
Dashboard: "Irányítópult",
"New Update": "Új frissítés",
Language: "Nyelv",
Appearance: "Megjelenés",
Theme: "Téma",
General: "Általános",
Version: "Verzió",
"Check Update On GitHub": "Frissítések keresése a GitHub-on",
List: "Lista",
Add: "Hozzáadás",
"Add New Monitor": "Új figyelő hozzáadása",
"Quick Stats": "Gyors statisztikák",
Up: "Működik",
Down: "Leállt",
Pending: "Függőben",
Unknown: "Ismeretlen",
Pause: "Szünet",
Name: "Név",
Status: "Állapot",
DateTime: "Időpont",
Message: "Üzenet",
"No important events": "Nincs fontos esemény",
Resume: "Folytatás",
Edit: "Szerkesztés",
Delete: "Törlés",
Current: "Aktuális",
Uptime: "Uptime",
"Cert Exp.": "Tanúsítvány lejár",
days: "napok",
day: "nap",
"-day": "-nap",
hour: "óra",
"-hour": "-óra",
Response: "Válasz",
Ping: "Ping",
"Monitor Type": "Figyelő típusa",
Keyword: "Kulcsszó",
"Friendly Name": "Rövid név",
URL: "URL",
Hostname: "Hostnév",
Port: "Port",
"Heartbeat Interval": "Heartbeat időköz",
Retries: "Újrapróbálkozás",
"Heartbeat Retry Interval": "Heartbeat újrapróbálkozások időköze",
Advanced: "Haladó",
"Upside Down Mode": "Fordított mód",
"Max. Redirects": "Max. átirányítás",
"Accepted Status Codes": "Elfogadott állapot kódok",
Save: "Mentés",
Notifications: "Értesítések",
"Not available, please setup.": "Nem elérhető, állítsa be.",
"Setup Notification": "Értesítés beállítása",
Light: "Világos",
Dark: "Sötét",
Auto: "Auto",
"Theme - Heartbeat Bar": "Téma - Heartbeat Bar",
Normal: "Normal",
Bottom: "Nyomógomb",
None: "Nincs",
Timezone: "Időzóna",
"Search Engine Visibility": "Látható a keresőmotoroknak",
"Allow indexing": "Indexelés engedélyezése",
"Discourage search engines from indexing site": "Keresőmotorok elriasztása az oldal indexelésétől",
"Change Password": "Jelszó változtatása",
"Current Password": "Jelenlegi jelszó",
"New Password": "Új jelszó",
"Repeat New Password": "Ismételje meg az új jelszót",
"Update Password": "Jelszó módosítása",
"Disable Auth": "Hitelesítés tiltása",
"Enable Auth": "Hitelesítés engedélyezése",
Logout: "Kijelenetkezés",
Leave: "Elhagy",
"I understand, please disable": "Megértettem, kérem tilsa le",
Confirm: "Megerősítés",
Yes: "Igen",
No: "Nem",
Username: "Felhasználónév",
Password: "Jelszó",
"Remember me": "Emlékezzen rám",
Login: "Bejelentkezés",
"No Monitors, please": "Nincs figyelő, kérem",
"add one": "adjon hozzá egyet",
"Notification Type": "Értesítés típusa",
Email: "Email",
Test: "Teszt",
"Certificate Info": "Tanúsítvány információk",
"Resolver Server": "Resolver szerver",
"Resource Record Type": "Resource Record típusa",
"Last Result": "Utolsó eredmény",
"Create your admin account": "Hozza létre az adminisztrátor felhasználót",
"Repeat Password": "Jelszó ismétlése",
"Import Backup": "Mentés importálása",
"Export Backup": "Mentés exportálása",
Export: "Exportálás",
Import: "Importálás",
respTime: "Válaszidő (ms)",
notAvailableShort: "N/A",
"Default enabled": "Alapértelmezetten engedélyezett",
"Apply on all existing monitors": "Alkalmazza az összes figyelőre",
Create: "Létrehozás",
"Clear Data": "Adatok törlése",
Events: "Események",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "Ki tudja menteni az összes figyelőt és értesítést egy JSON fájlba.",
backupDescription2: "Ui.: Történeti és esemény adatokat nem tartalmaz.",
backupDescription3: "Érzékeny adatok, pl. szolgáltatás kulcsok is vannak az export fájlban. Figyelmesen őrizze!",
alertNoFile: "Válaszzon ki egy fájlt az importáláshoz.",
alertWrongFileType: "Válasszon egy JSON fájlt.",
"Clear all statistics": "Összes statisztika törlése",
"Skip existing": "Meglévő kihagyása",
Overwrite: "Felülírás",
Options: "Opciók",
"Keep both": "Mindegyiket tartsa meg",
"Verify Token": "Token ellenőrzése",
"Setup 2FA": "2FA beállítása",
"Enable 2FA": "2FA engedélyezése",
"Disable 2FA": "2FA toltása",
"2FA Settings": "2FA beállítások",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Aktív",
Inactive: "Inaktív",
Token: "Token",
"Show URI": "URI megmutatása",
Tags: "Cimkék",
"Add New below or Select...": "Adjon hozzá lentre vagy válasszon...",
"Tag with this name already exist.": "Ilyen nevű cimke már létezik.",
"Tag with this value already exist.": "Ilyen értékű cimke már létezik.",
color: "szín",
"value (optional)": "érték (opcionális)",
Gray: "Szürke",
Red: "Piros",
Orange: "Narancs",
Green: "Zöld",
Blue: "Kék",
Indigo: "Indigó",
Purple: "Lila",
Pink: "Rózsaszín",
"Search...": "Keres...",
"Avg. Ping": "Átl. ping",
"Avg. Response": "Átl. válasz",
"Entry Page": "Nyitólap",
statusPageNothing: "Semmi nincs itt, kérem, adjon hozzá egy figyelőt.",
"No Services": "Nincs szolgáltatás",
"All Systems Operational": "Minden rendszer működik",
"Partially Degraded Service": "Részlegesen leállt szolgáltatás",
"Degraded Service": "Leállt szolgáltatás",
"Add Group": "Csoport hozzáadása",
"Add a monitor": "Figyelő hozzáadása",
"Edit Status Page": "Sátusz oldal szerkesztése",
"Go to Dashboard": "Menj az irányítópulthoz",
};

View File

@ -10,7 +10,7 @@
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom"> <header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> <router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" /> <object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title">Uptime Kuma</span> <span class="fs-4 title">{{ $t("Uptime Kuma") }}</span>
</router-link> </router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3"> <a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
@ -23,12 +23,12 @@
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }} <font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
</a> </a>
</li> </li>
<li class="nav-item me-2"> <li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }} <font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
</router-link> </router-link>
</li> </li>
<li class="nav-item"> <li v-if="$root.loggedIn" class="nav-item">
<router-link to="/settings" class="nav-link"> <router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> {{ $t("Settings") }} <font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link> </router-link>

View File

@ -1,7 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -14,7 +14,7 @@ export default {
data() { data() {
return { return {
userTimezone: localStorage.timezone || "auto", userTimezone: localStorage.timezone || "auto",
} };
}, },
methods: { methods: {
@ -47,11 +47,11 @@ export default {
computed: { computed: {
timezone() { timezone() {
if (this.userTimezone === "auto") { if (this.userTimezone === "auto") {
return dayjs.tz.guess() return dayjs.tz.guess();
} }
return this.userTimezone return this.userTimezone;
}, },
} }
} };

View File

@ -36,5 +36,13 @@ export default {
return result; return result;
}, },
baseURL() {
if (env === "development" || localStorage.dev === "dev") {
return axios.defaults.baseURL;
} else {
return location.protocol + "//" + location.host;
}
}
} }
}; };

View File

@ -293,6 +293,9 @@ export default {
}, },
getMonitorList(callback) { getMonitorList(callback) {
if (! callback) {
callback = () => { };
}
socket.emit("getMonitorList", callback); socket.emit("getMonitorList", callback);
}, },

View File

@ -57,6 +57,7 @@
v-model="page" v-model="page"
:records="importantHeartBeatList.length" :records="importantHeartBeatList.length"
:per-page="perPage" :per-page="perPage"
:options="paginationConfig"
/> />
</div> </div>
</div> </div>
@ -81,6 +82,17 @@ export default {
page: 1, page: 1,
perPage: 25, perPage: 25,
heartBeatList: [], heartBeatList: [],
paginationConfig: {
texts:{
count:`${this.$t("Showing {from} to {to} of {count} records")}|{count} ${this.$t("records")}|${this.$t("One record")}`,
first:this.$t("First"),
last:this.$t("Last"),
nextPage:'>',
nextChunk:'>>',
prevPage:'<',
prevChunk:'<<'
}
}
} }
}, },
computed: { computed: {

View File

@ -181,6 +181,7 @@
v-model="page" v-model="page"
:records="importantHeartBeatList.length" :records="importantHeartBeatList.length"
:per-page="perPage" :per-page="perPage"
:options="paginationConfig"
/> />
</div> </div>
</div> </div>
@ -237,6 +238,17 @@ export default {
heartBeatList: [], heartBeatList: [],
toggleCertInfoBox: false, toggleCertInfoBox: false,
showPingChartBox: true, showPingChartBox: true,
paginationConfig: {
texts:{
count:`${this.$t("Showing {from} to {to} of {count} records")}|{count} ${this.$t("records")}|${this.$t("One record")}`,
first:this.$t("First"),
last:this.$t("Last"),
nextPage:'>',
nextChunk:'>>',
prevPage:'<',
prevChunk:'<<'
}
}
} }
}, },
computed: { computed: {

View File

@ -26,19 +26,35 @@
<option value="dns"> <option value="dns">
DNS DNS
</option> </option>
<option value="push">
Push
</option>
</select> </select>
</div> </div>
<!-- Friendly Name -->
<div class="my-3"> <div class="my-3">
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label> <label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label> <label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<!-- Push URL -->
<div v-if="monitor.type === 'push' " class="my-3">
<label for="push-url" class="form-label">{{ $t("Push URL") }}</label>
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
<div class="form-text">
You should call this url every {{ monitor.interval }} seconds.<br />
Optional parameters: msg, ping
</div>
</div>
<!-- Keyword -->
<div v-if="monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label> <label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
@ -93,6 +109,7 @@
</div> </div>
</template> </template>
<!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
@ -210,13 +227,17 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { useToast } from "vue-toastification" import CopyableInput from "../components/CopyableInput.vue";
import VueMultiselect from "vue-multiselect"
import { isDev } from "../util.ts"; import { useToast } from "vue-toastification";
const toast = useToast() import VueMultiselect from "vue-multiselect";
import { genSecret, isDev } from "../util.ts";
const toast = useToast();
export default { export default {
components: { components: {
CopyableInput,
NotificationDialog, NotificationDialog,
TagsManager, TagsManager,
VueMultiselect, VueMultiselect,
@ -236,7 +257,7 @@ export default {
ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))", ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))",
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
hostnameRegexPattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" hostnameRegexPattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"
} };
}, },
computed: { computed: {
@ -253,23 +274,41 @@ export default {
pageName() { pageName() {
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit"); return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
}, },
isAdd() { isAdd() {
return this.$route.path === "/add"; return this.$route.path === "/add";
}, },
isEdit() { isEdit() {
return this.$route.path.startsWith("/edit"); return this.$route.path.startsWith("/edit");
}, },
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
}
}, },
watch: { watch: {
"$route.fullPath"() { "$route.fullPath"() {
this.init(); this.init();
}, },
"monitor.interval"(value, oldValue) { "monitor.interval"(value, oldValue) {
// Link interval and retryInerval if they are the same value. // Link interval and retryInerval if they are the same value.
if (this.monitor.retryInterval === oldValue) { if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value; this.monitor.retryInterval = value;
} }
},
"monitor.type"() {
if (this.monitor.type === "push") {
if (! this.monitor.pushToken) {
this.monitor.pushToken = genSecret(10);
} }
}
}
}, },
mounted() { mounted() {
this.init(); this.init();
@ -320,7 +359,7 @@ export default {
accepted_statuscodes: ["200-299"], accepted_statuscodes: ["200-299"],
dns_resolve_type: "A", dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1", dns_resolve_server: "1.1.1.1",
} };
for (let i = 0; i < this.$root.notificationList.length; i++) { for (let i = 0; i < this.$root.notificationList.length; i++) {
if (this.$root.notificationList[i].isDefault == true) { if (this.$root.notificationList[i].isDefault == true) {
@ -337,9 +376,9 @@ export default {
this.monitor.retryInterval = this.monitor.interval; this.monitor.retryInterval = this.monitor.interval;
} }
} else { } else {
toast.error(res.msg) toast.error(res.msg);
} }
}) });
} }
}, },
@ -356,13 +395,13 @@ export default {
toast.success(res.msg); toast.success(res.msg);
this.processing = false; this.processing = false;
this.$root.getMonitorList(); this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID) this.$router.push("/dashboard/" + res.monitorID);
} else { } else {
toast.error(res.msg); toast.error(res.msg);
this.processing = false; this.processing = false;
} }
}) });
} else { } else {
await this.$refs.tagsManager.submit(this.monitor.id); await this.$refs.tagsManager.submit(this.monitor.id);
@ -370,7 +409,7 @@ export default {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
this.init(); this.init();
}) });
} }
}, },
@ -380,61 +419,9 @@ export default {
this.monitor.notificationIDList[id] = true; this.monitor.notificationIDList[id] = true;
}, },
}, },
} };
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
@import "../assets/vars.scss";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
min-height: 38px;
padding: 6px 40px 0 8px;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: 50rem;
margin-bottom: 0;
padding: 6px 26px 6px 10px;
background: $primary !important;
}
.multiselect__placeholder {
font-size: 1rem;
padding-left: 6px;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
opacity: 0.67;
}
.multiselect__input, .multiselect__single {
line-height: 14px;
margin-bottom: 0;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
}
</style>
<style scoped> <style scoped>
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;

View File

@ -226,7 +226,7 @@
{{ $t("Setup Notification") }} {{ $t("Setup Notification") }}
</button> </button>
<h2 class="mt-5">Info</h2> <h2 class="mt-5">{{ $t("Info") }}</h2>
{{ $t("Version") }}: {{ $root.info.version }} <br /> {{ $t("Version") }}: {{ $root.info.version }} <br />
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a> <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
@ -316,6 +316,24 @@
<p>Пожалуйста, используйте с осторожностью.</p> <p>Пожалуйста, используйте с осторожностью.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'fa' ">
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
<p>لطفا از این امکان با دقت استفاده کنید.</p>
</template>
<template v-else-if="$i18n.locale === 'bg-BG' ">
<p>Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?</p>
<p>Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access.</p>
<p>Моля, използвайте внимателно.</p>
</template>
<template v-else-if="$i18n.locale === 'hu' ">
<p>Biztos benne, hogy <strong>kikapcsolja a hitelesítést</strong>?</p>
<p>Akkor érdemes, ha <strong>van 3rd-party hitelesítés</strong> az Uptime Kuma-t megelőzően mint a Cloudflare Access.</p>
<p>Használja megfontoltan!</p>
</template>
<!-- English (en) --> <!-- English (en) -->
<template v-else> <template v-else>
<p>Are you sure want to <strong>disable auth</strong>?</p> <p>Are you sure want to <strong>disable auth</strong>?</p>
@ -344,8 +362,9 @@ import TwoFADialog from "../components/TwoFADialog.vue";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
import { timezoneList } from "../util-frontend"; import { timezoneList, setPageLocale } from "../util-frontend";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
const toast = useToast(); const toast = useToast();
export default { export default {
@ -381,6 +400,7 @@ export default {
"$i18n.locale"() { "$i18n.locale"() {
localStorage.locale = this.$i18n.locale; localStorage.locale = this.$i18n.locale;
setPageLocale()
}, },
}, },

View File

@ -46,8 +46,8 @@
</template> </template>
<script> <script>
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast();
export default { export default {
data() { data() {
@ -56,7 +56,7 @@ export default {
username: "", username: "",
password: "", password: "",
repeatPassword: "", repeatPassword: "",
} };
}, },
watch: { watch: {
"$i18n.locale"() { "$i18n.locale"() {
@ -66,7 +66,7 @@ export default {
mounted() { mounted() {
this.$root.getSocket().emit("needSetup", (needSetup) => { this.$root.getSocket().emit("needSetup", (needSetup) => {
if (! needSetup) { if (! needSetup) {
this.$router.push("/") this.$router.push("/");
} }
}); });
}, },
@ -75,31 +75,30 @@ export default {
this.processing = true; this.processing = true;
if (this.password !== this.repeatPassword) { if (this.password !== this.repeatPassword) {
toast.error("Repeat password do not match.") toast.error("Repeat password do not match.");
this.processing = false; this.processing = false;
return; return;
} }
this.$root.getSocket().emit("setup", this.username, this.password, (res) => { this.$root.getSocket().emit("setup", this.username, this.password, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res) this.$root.toastRes(res);
if (res.ok) { if (res.ok) {
this.processing = true; this.processing = true;
this.$root.login(this.username, this.password, "", (res) => { this.$root.login(this.username, this.password, "", () => {
this.processing = false; this.processing = false;
this.$router.push("/") this.$router.push("/");
}) });
} }
}) });
}, },
}, },
} };
</script> </script>
<style scoped> <style lang="scss" scoped>
.form-container { .form-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -107,6 +106,26 @@ export default {
padding-bottom: 40px; padding-bottom: 40px;
} }
.form-floating {
> .form-select {
padding-left: 1.3rem;
padding-top: 1.525rem;
line-height: 1.35;
~ label {
padding-left: 1.3rem;
}
}
> label {
padding-left: 1.3rem;
}
> .form-control {
padding-left: 1.3rem;
}
}
.form { .form {
width: 100%; width: 100%;

View File

@ -90,9 +90,9 @@
<!-- Incident Date --> <!-- Incident Date -->
<div class="date mt-3"> <div class="date mt-3">
Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br /> Created: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
<span v-if="incident.lastUpdatedDate"> <span v-if="incident.lastUpdatedDate">
Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }}) Last Updated: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
</span> </span>
</div> </div>
@ -157,7 +157,7 @@
</div> </div>
<div v-else> <div v-else>
<font-awesome-icon icon="question-circle" style="color: #efefef" /> <font-awesome-icon icon="question-circle" style="color: #efefef;" />
</div> </div>
</template> </template>
</div> </div>
@ -197,7 +197,7 @@
</div> </div>
<footer class="mt-5 mb-4"> <footer class="mt-5 mb-4">
Powered by <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> {{ $t("Powered By") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" )}}</a>
</footer> </footer>
</div> </div>
</template> </template>
@ -343,14 +343,6 @@ export default {
return this.overallStatus === STATUS_PAGE_ALL_DOWN; return this.overallStatus === STATUS_PAGE_ALL_DOWN;
}, },
createdDateFromNow() {
return dayjs.utc(this.incident.createdDate).fromNow();
},
lastUpdatedDateFromNow() {
return dayjs.utc(this.incident. lastUpdatedDate).fromNow();
}
}, },
watch: { watch: {
@ -548,7 +540,12 @@ export default {
this.$root.getSocket().emit("unpinIncident", () => { this.$root.getSocket().emit("unpinIncident", () => {
this.incident = null; this.incident = null;
}); });
} },
dateFromNow(date) {
return dayjs.utc(date).fromNow();
},
} }
}; };
</script> </script>

View File

@ -6,9 +6,9 @@ import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue"; import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue"; import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue"; import List from "./pages/List.vue";
import Settings from "./pages/Settings.vue"; const Settings = () => import("./pages/Settings.vue");
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
import StatusPage from "./pages/StatusPage.vue"; const StatusPage = () => import("./pages/StatusPage.vue");
import Entry from "./pages/Entry.vue"; import Entry from "./pages/Entry.vue";
const routes = [ const routes = [

View File

@ -2,6 +2,7 @@ import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import timezones from "timezones-list"; import timezones from "timezones-list";
import { localeDirection, currentLocale } from "./i18n";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -30,7 +31,7 @@ export function timezoneList() {
time: getTimezoneOffset(timezone.tzCode), time: getTimezoneOffset(timezone.tzCode),
}); });
} catch (e) { } catch (e) {
console.log("Skip Timezone: " + timezone.tzCode); // Skipping not supported timezone.tzCode by dayjs
} }
} }
@ -48,3 +49,9 @@ export function timezoneList() {
return result; return result;
} }
export function setPageLocale() {
const html = document.documentElement
html.setAttribute('lang', currentLocale() )
html.setAttribute('dir', localeDirection() )
}

View File

@ -7,7 +7,7 @@
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; exports.genSecret = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const _dayjs = require("dayjs"); const _dayjs = require("dayjs");
const dayjs = _dayjs; const dayjs = _dayjs;
exports.isDev = process.env.NODE_ENV === "development"; exports.isDev = process.env.NODE_ENV === "development";
@ -102,3 +102,13 @@ function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
} }
exports.getRandomInt = getRandomInt; exports.getRandomInt = getRandomInt;
function genSecret(length = 64) {
let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length;
for (let i = 0; i < length; i++) {
secret += chars.charAt(Math.floor(Math.random() * charsLength));
}
return secret;
}
exports.genSecret = genSecret;

View File

@ -113,3 +113,13 @@ export function getRandomInt(min: number, max: number) {
max = Math.floor(max); max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
} }
export function genSecret(length = 64) {
let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length;
for ( let i = 0; i < length; i++ ) {
secret += chars.charAt(Math.floor(Math.random() * charsLength));
}
return secret;
}

21
test/test.spec.js Normal file
View File

@ -0,0 +1,21 @@
beforeAll(() => {
});
afterAll(() => {
return console.log("Cleanup");
});
describe("Very Simple Test", () => {
const title = "Uptime Kuma";
beforeAll(async () => {
await page.goto("http://127.0.0.1:3002");
});
it(`should be titled "${title}"`, async () => {
await expect(page.title()).resolves.toMatch(title);
});
});

View File

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