mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into feat/monitor-list-improved-filtering
This commit is contained in:
		
							
								
								
									
										34
									
								
								.github/workflows/auto-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/auto-test.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
 | 
			
		||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
 | 
			
		||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 | 
			
		||||
 | 
			
		||||
name: Auto Test
 | 
			
		||||
@@ -21,8 +21,8 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [macos-latest, ubuntu-latest, windows-latest]
 | 
			
		||||
        node: [ 14, 16, 18, 20 ]
 | 
			
		||||
        os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
 | 
			
		||||
        node: [ 14, 18 ]
 | 
			
		||||
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
@@ -33,7 +33,7 @@ jobs:
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node }}
 | 
			
		||||
        cache: 'npm'
 | 
			
		||||
    - run: npm install npm@latest -g
 | 
			
		||||
    - run: npm install
 | 
			
		||||
    - run: npm run build
 | 
			
		||||
    - run: npm test
 | 
			
		||||
@@ -41,6 +41,29 @@ jobs:
 | 
			
		||||
        HEADLESS_TEST: 1
 | 
			
		||||
        JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
 | 
			
		||||
 | 
			
		||||
  # As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
 | 
			
		||||
  armv7-simple-test:
 | 
			
		||||
    needs: [ check-linters ]
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    timeout-minutes: 15
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ ARMv7 ]
 | 
			
		||||
        node: [ 14.21.3, 18.16.1 ]
 | 
			
		||||
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: git config --global core.autocrlf false  # Mainly for Windows
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Use Node.js ${{ matrix.node }}
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node }}
 | 
			
		||||
      - run: npm install npm@latest -g
 | 
			
		||||
      - run: npm ci --production
 | 
			
		||||
 | 
			
		||||
  check-linters:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +75,6 @@ jobs:
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: 14
 | 
			
		||||
        cache: 'npm'
 | 
			
		||||
    - run: npm install
 | 
			
		||||
    - run: npm run lint
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +89,6 @@ jobs:
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: 14
 | 
			
		||||
        cache: 'npm'
 | 
			
		||||
    - run: npm install
 | 
			
		||||
    - run: npm run build
 | 
			
		||||
    - run: npm run cy:test
 | 
			
		||||
@@ -83,7 +104,6 @@ jobs:
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: 14
 | 
			
		||||
        cache: 'npm'
 | 
			
		||||
    - run: npm install
 | 
			
		||||
    - run: npm run build
 | 
			
		||||
    - run: npm run cy:run:unit
 | 
			
		||||
 
 | 
			
		||||
@@ -47,17 +47,17 @@ Here are some references:
 | 
			
		||||
 | 
			
		||||
❌ Won't Merge
 | 
			
		||||
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet) 
 | 
			
		||||
- Do not pass auto test
 | 
			
		||||
- Do not pass the auto test
 | 
			
		||||
- Any breaking changes
 | 
			
		||||
- Duplicated pull request
 | 
			
		||||
- Duplicated pull requests
 | 
			
		||||
- Buggy
 | 
			
		||||
- UI/UX is not close to Uptime Kuma 
 | 
			
		||||
- Existing logic is completely modified or deleted for no reason
 | 
			
		||||
- A function that is completely out of scope
 | 
			
		||||
- Convert existing code into other programming languages
 | 
			
		||||
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
 | 
			
		||||
- Modifications or deletions of existing logic without a valid reason.
 | 
			
		||||
- Adding functions that is completely out of scope
 | 
			
		||||
- Converting existing code into other programming languages
 | 
			
		||||
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
 | 
			
		||||
 | 
			
		||||
The above cases cannot cover all situations.
 | 
			
		||||
The above cases may not cover all possible situations.
 | 
			
		||||
 | 
			
		||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
 | 
			
		||||
 | 
			
		||||
## ⭐ Features
 | 
			
		||||
 | 
			
		||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
 | 
			
		||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
 | 
			
		||||
* Fancy, Reactive, Fast UI/UX
 | 
			
		||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
 | 
			
		||||
* 20 second intervals
 | 
			
		||||
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
 | 
			
		||||
 | 
			
		||||
### Windows Portable (x64)
 | 
			
		||||
 | 
			
		||||
https://github.com/louislam/uptime-kuma/releases/download/1.21.0/uptime-kuma-win64-portable-1.0.0.zip
 | 
			
		||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
 | 
			
		||||
 | 
			
		||||
### Advanced Installation
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
import visualizer from "rollup-plugin-visualizer";
 | 
			
		||||
import viteCompression from "vite-plugin-compression";
 | 
			
		||||
import commonjs from "vite-plugin-commonjs";
 | 
			
		||||
 | 
			
		||||
const postCssScss = require("postcss-scss");
 | 
			
		||||
const postcssRTLCSS = require("postcss-rtlcss");
 | 
			
		||||
@@ -18,6 +19,7 @@ export default defineConfig({
 | 
			
		||||
        "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
 | 
			
		||||
    },
 | 
			
		||||
    plugins: [
 | 
			
		||||
        commonjs(),
 | 
			
		||||
        vue(),
 | 
			
		||||
        legacy({
 | 
			
		||||
            targets: [ "since 2015" ],
 | 
			
		||||
@@ -42,6 +44,9 @@ export default defineConfig({
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    build: {
 | 
			
		||||
        commonjsOptions: {
 | 
			
		||||
            include: [ /.js$/ ],
 | 
			
		||||
        },
 | 
			
		||||
        rollupOptions: {
 | 
			
		||||
            output: {
 | 
			
		||||
                manualChunks(id, { getModuleInfo, getModuleIds }) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								db/patch-add-invert-keyword.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/patch-add-invert-keyword.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
 | 
			
		||||
BEGIN TRANSACTION;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE monitor
 | 
			
		||||
    ADD invert_keyword BOOLEAN default 0 not null;
 | 
			
		||||
 | 
			
		||||
COMMIT;
 | 
			
		||||
							
								
								
									
										10
									
								
								db/patch-added-json-query.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/patch-added-json-query.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
 | 
			
		||||
BEGIN TRANSACTION;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE monitor
 | 
			
		||||
	ADD json_path TEXT;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE monitor
 | 
			
		||||
	ADD expected_value VARCHAR(255);
 | 
			
		||||
 | 
			
		||||
COMMIT;
 | 
			
		||||
@@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
 | 
			
		||||
FROM louislam/uptime-kuma:base-debian AS release
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
ENV UPTIME_KUMA_IS_CONTAINER=1
 | 
			
		||||
 | 
			
		||||
# Copy app files from build layer
 | 
			
		||||
COPY --from=build /app /app
 | 
			
		||||
 | 
			
		||||
@@ -70,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
 | 
			
		||||
RUN npm ci
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000 3001
 | 
			
		||||
VOLUME ["/app/data"]
 | 
			
		||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
 | 
			
		||||
CMD ["npm", "run", "start-pr-test"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
 | 
			
		||||
  <Costura />
 | 
			
		||||
  <Costura DisableCompression='true' IncludeDebugSymbols='false' />
 | 
			
		||||
</Weavers>
 | 
			
		||||
@@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
 | 
			
		||||
// set of attributes. Change these attribute values to modify the information
 | 
			
		||||
// associated with an assembly.
 | 
			
		||||
[assembly: AssemblyTitle("Uptime Kuma")]
 | 
			
		||||
[assembly: AssemblyDescription("")]
 | 
			
		||||
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
 | 
			
		||||
[assembly: AssemblyConfiguration("")]
 | 
			
		||||
[assembly: AssemblyCompany("")]
 | 
			
		||||
[assembly: AssemblyCompany("Uptime Kuma")]
 | 
			
		||||
[assembly: AssemblyProduct("Uptime Kuma")]
 | 
			
		||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
 | 
			
		||||
[assembly: AssemblyTrademark("")]
 | 
			
		||||
@@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
 | 
			
		||||
[assembly: ComVisible(false)]
 | 
			
		||||
 | 
			
		||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
 | 
			
		||||
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
 | 
			
		||||
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
 | 
			
		||||
 | 
			
		||||
// Version information for an assembly consists of the following four values:
 | 
			
		||||
//
 | 
			
		||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
 | 
			
		||||
// You can specify all the values or you can default the Build and Revision Numbers
 | 
			
		||||
// by using the '*' as shown below:
 | 
			
		||||
// [assembly: AssemblyVersion("1.0.*")]
 | 
			
		||||
[assembly: AssemblyVersion("1.0.0.0")]
 | 
			
		||||
[assembly: AssemblyFileVersion("1.0.0.0")]
 | 
			
		||||
[assembly: AssemblyVersion("1.0.1.0")]
 | 
			
		||||
[assembly: AssemblyFileVersion("1.0.1.0")]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								extra/test-docker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								extra/test-docker.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
// Check if docker is running
 | 
			
		||||
const { exec } = require("child_process");
 | 
			
		||||
 | 
			
		||||
exec("docker ps", (err, stdout, stderr) => {
 | 
			
		||||
    if (err) {
 | 
			
		||||
        console.error("Docker is not running. Please start docker and try again.");
 | 
			
		||||
        process.exit(1);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										5996
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5996
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								package.json
									
									
									
									
									
								
							@@ -1,13 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "uptime-kuma",
 | 
			
		||||
    "version": "1.22.0-beta.0",
 | 
			
		||||
    "version": "1.22.1",
 | 
			
		||||
    "license": "MIT",
 | 
			
		||||
    "repository": {
 | 
			
		||||
        "type": "git",
 | 
			
		||||
        "url": "https://github.com/louislam/uptime-kuma.git"
 | 
			
		||||
    },
 | 
			
		||||
    "engines": {
 | 
			
		||||
        "node": "14.* || >=16.*"
 | 
			
		||||
        "node": "14.* || 16.* || 18.*"
 | 
			
		||||
    },
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "install-legacy": "npm install",
 | 
			
		||||
@@ -34,12 +34,12 @@
 | 
			
		||||
        "build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
 | 
			
		||||
        "build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
 | 
			
		||||
        "build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
 | 
			
		||||
        "build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
 | 
			
		||||
        "build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
 | 
			
		||||
        "build-docker-nightly-alpine": "docker buildx build -f docker/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 -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
 | 
			
		||||
        "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
 | 
			
		||||
        "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
 | 
			
		||||
        "setup": "git checkout 1.21.3 && npm ci --production && npm run download-dist",
 | 
			
		||||
        "setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist",
 | 
			
		||||
        "download-dist": "node extra/download-dist.js",
 | 
			
		||||
        "mark-as-nightly": "node extra/mark-as-nightly.js",
 | 
			
		||||
        "reset-password": "node extra/reset-password.js",
 | 
			
		||||
@@ -54,8 +54,8 @@
 | 
			
		||||
        "simple-mqtt-server": "node extra/simple-mqtt-server.js",
 | 
			
		||||
        "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
 | 
			
		||||
        "ncu-patch": "npm-check-updates -u -t patch",
 | 
			
		||||
        "release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
 | 
			
		||||
        "release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta .  --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
 | 
			
		||||
        "release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
 | 
			
		||||
        "release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta .  --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
 | 
			
		||||
        "git-remove-tag": "git tag -d",
 | 
			
		||||
        "build-dist-and-restart": "npm run build && npm run start-server-dev",
 | 
			
		||||
        "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
 | 
			
		||||
@@ -84,27 +84,29 @@
 | 
			
		||||
        "command-exists": "~1.2.9",
 | 
			
		||||
        "compare-versions": "~3.6.0",
 | 
			
		||||
        "compression": "~1.7.4",
 | 
			
		||||
        "croner": "^6.0.3",
 | 
			
		||||
        "croner": "~6.0.5",
 | 
			
		||||
        "dayjs": "~1.11.5",
 | 
			
		||||
        "dotenv": "~16.0.3",
 | 
			
		||||
        "express": "~4.17.3",
 | 
			
		||||
        "express-basic-auth": "~1.2.1",
 | 
			
		||||
        "express-static-gzip": "~2.1.7",
 | 
			
		||||
        "form-data": "~4.0.0",
 | 
			
		||||
        "gamedig": "^4.0.5",
 | 
			
		||||
        "gamedig": "~4.0.5",
 | 
			
		||||
        "http-graceful-shutdown": "~3.1.7",
 | 
			
		||||
        "http-proxy-agent": "~5.0.0",
 | 
			
		||||
        "https-proxy-agent": "~5.0.1",
 | 
			
		||||
        "iconv-lite": "~0.6.3",
 | 
			
		||||
        "jsesc": "~3.0.2",
 | 
			
		||||
        "jsonata": "^2.0.3",
 | 
			
		||||
        "jsonwebtoken": "~9.0.0",
 | 
			
		||||
        "jwt-decode": "~3.1.2",
 | 
			
		||||
        "limiter": "~2.1.0",
 | 
			
		||||
        "liquidjs": "^10.7.0",
 | 
			
		||||
        "mongodb": "~4.14.0",
 | 
			
		||||
        "mqtt": "~4.3.7",
 | 
			
		||||
        "mssql": "~8.1.4",
 | 
			
		||||
        "mysql2": "~2.3.3",
 | 
			
		||||
        "nanoid": "^3.3.4",
 | 
			
		||||
        "nanoid": "~3.3.4",
 | 
			
		||||
        "node-cloudflared-tunnel": "~1.0.9",
 | 
			
		||||
        "node-radius-client": "~1.0.0",
 | 
			
		||||
        "nodemailer": "~6.6.5",
 | 
			
		||||
@@ -112,11 +114,12 @@
 | 
			
		||||
        "password-hash": "~1.2.2",
 | 
			
		||||
        "pg": "~8.8.0",
 | 
			
		||||
        "pg-connection-string": "~2.5.0",
 | 
			
		||||
        "playwright-core": "~1.35.1",
 | 
			
		||||
        "prom-client": "~13.2.0",
 | 
			
		||||
        "prometheus-api-metrics": "~3.2.1",
 | 
			
		||||
        "protobufjs": "~7.1.1",
 | 
			
		||||
        "protobufjs": "~7.2.4",
 | 
			
		||||
        "qs": "~6.10.4",
 | 
			
		||||
        "redbean-node": "~0.2.0",
 | 
			
		||||
        "redbean-node": "~0.3.0",
 | 
			
		||||
        "redis": "~4.5.1",
 | 
			
		||||
        "socket.io": "~4.6.1",
 | 
			
		||||
        "socket.io-client": "~4.6.1",
 | 
			
		||||
@@ -127,7 +130,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@actions/github": "~5.0.1",
 | 
			
		||||
        "@babel/eslint-parser": "~7.17.0",
 | 
			
		||||
        "@babel/eslint-parser": "^7.22.7",
 | 
			
		||||
        "@babel/preset-env": "^7.15.8",
 | 
			
		||||
        "@fortawesome/fontawesome-svg-core": "~1.2.36",
 | 
			
		||||
        "@fortawesome/free-regular-svg-icons": "~5.15.4",
 | 
			
		||||
@@ -135,9 +138,9 @@
 | 
			
		||||
        "@fortawesome/vue-fontawesome": "~3.0.0-5",
 | 
			
		||||
        "@popperjs/core": "~2.10.2",
 | 
			
		||||
        "@types/bootstrap": "~5.1.9",
 | 
			
		||||
        "@vitejs/plugin-legacy": "~2.1.0",
 | 
			
		||||
        "@vitejs/plugin-vue": "~3.1.0",
 | 
			
		||||
        "@vue/compiler-sfc": "~3.2.36",
 | 
			
		||||
        "@vitejs/plugin-legacy": "~4.1.0",
 | 
			
		||||
        "@vitejs/plugin-vue": "~4.2.3",
 | 
			
		||||
        "@vue/compiler-sfc": "~3.3.4",
 | 
			
		||||
        "@vuepic/vue-datepicker": "~3.4.8",
 | 
			
		||||
        "aedes": "^0.46.3",
 | 
			
		||||
        "babel-plugin-rewire": "~1.2.0",
 | 
			
		||||
@@ -148,16 +151,16 @@
 | 
			
		||||
        "core-js": "~3.26.1",
 | 
			
		||||
        "cronstrue": "~2.24.0",
 | 
			
		||||
        "cross-env": "~7.0.3",
 | 
			
		||||
        "cypress": "^10.1.0",
 | 
			
		||||
        "cypress": "^12.17.0",
 | 
			
		||||
        "delay": "^5.0.0",
 | 
			
		||||
        "dns2": "~2.0.1",
 | 
			
		||||
        "dompurify": "~2.4.3",
 | 
			
		||||
        "eslint": "~8.14.0",
 | 
			
		||||
        "eslint-plugin-vue": "~8.7.1",
 | 
			
		||||
        "favico.js": "~0.3.10",
 | 
			
		||||
        "jest": "~27.2.5",
 | 
			
		||||
        "jest": "~29.6.1",
 | 
			
		||||
        "marked": "~4.2.5",
 | 
			
		||||
        "node-ssh": "~13.0.1",
 | 
			
		||||
        "node-ssh": "~13.1.0",
 | 
			
		||||
        "postcss-html": "~1.5.0",
 | 
			
		||||
        "postcss-rtlcss": "~3.7.2",
 | 
			
		||||
        "postcss-scss": "~4.0.4",
 | 
			
		||||
@@ -165,15 +168,16 @@
 | 
			
		||||
        "qrcode": "~1.5.0",
 | 
			
		||||
        "rollup-plugin-visualizer": "^5.6.0",
 | 
			
		||||
        "sass": "~1.42.1",
 | 
			
		||||
        "stylelint": "~14.7.1",
 | 
			
		||||
        "stylelint": "^15.10.1",
 | 
			
		||||
        "stylelint-config-standard": "~25.0.0",
 | 
			
		||||
        "terser": "~5.15.0",
 | 
			
		||||
        "timezones-list": "~3.0.1",
 | 
			
		||||
        "typescript": "~4.4.4",
 | 
			
		||||
        "v-pagination-3": "~0.1.7",
 | 
			
		||||
        "vite": "~3.2.7",
 | 
			
		||||
        "vite": "~4.4.1",
 | 
			
		||||
        "vite-plugin-commonjs": "^0.8.0",
 | 
			
		||||
        "vite-plugin-compression": "^0.5.1",
 | 
			
		||||
        "vue": "~3.2.47",
 | 
			
		||||
        "vue": "~3.3.4",
 | 
			
		||||
        "vue-chartjs": "~5.2.0",
 | 
			
		||||
        "vue-confirm-dialog": "~1.0.2",
 | 
			
		||||
        "vue-contenteditable": "~3.0.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
 | 
			
		||||
const passwordHash = require("./password-hash");
 | 
			
		||||
const { R } = require("redbean-node");
 | 
			
		||||
const { setting } = require("./util-server");
 | 
			
		||||
const { log } = require("../src/util");
 | 
			
		||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
@@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
 | 
			
		||||
    apiRateLimiter.pass(null, 0).then((pass) => {
 | 
			
		||||
        if (pass) {
 | 
			
		||||
            verifyAPIKey(password).then((valid) => {
 | 
			
		||||
                if (!valid) {
 | 
			
		||||
                    log.warn("api-auth", "Failed API auth attempt: invalid API Key");
 | 
			
		||||
                }
 | 
			
		||||
                callback(null, valid);
 | 
			
		||||
                // Only allow a set number of api requests per minute
 | 
			
		||||
                // (currently set to 60)
 | 
			
		||||
                apiRateLimiter.removeTokens(1);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
 | 
			
		||||
            callback(null, false);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
@@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
 | 
			
		||||
                callback(null, user != null);
 | 
			
		||||
 | 
			
		||||
                if (user == null) {
 | 
			
		||||
                    log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
 | 
			
		||||
                    loginRateLimiter.removeTokens(1);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
 | 
			
		||||
            callback(null, false);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,33 @@
 | 
			
		||||
const { setSetting, setting } = require("./util-server");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const compareVersions = require("compare-versions");
 | 
			
		||||
const { log } = require("../src/util");
 | 
			
		||||
 | 
			
		||||
exports.version = require("../package.json").version;
 | 
			
		||||
exports.latestVersion = null;
 | 
			
		||||
 | 
			
		||||
// How much time in ms to wait between update checks
 | 
			
		||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
 | 
			
		||||
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
 | 
			
		||||
 | 
			
		||||
let interval;
 | 
			
		||||
 | 
			
		||||
/** Start 48 hour check interval */
 | 
			
		||||
exports.startInterval = () => {
 | 
			
		||||
    let check = async () => {
 | 
			
		||||
        if (await setting("checkUpdate") === false) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.debug("update-checker", "Retrieving latest versions");
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await axios.get("https://uptime.kuma.pet/version");
 | 
			
		||||
            const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
 | 
			
		||||
 | 
			
		||||
            // For debug
 | 
			
		||||
            if (process.env.TEST_CHECK_VERSION === "1") {
 | 
			
		||||
                res.data.slow = "1000.0.0";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (await setting("checkUpdate") === false) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let checkBeta = await setting("checkBeta");
 | 
			
		||||
 | 
			
		||||
            if (checkBeta && res.data.beta) {
 | 
			
		||||
@@ -35,12 +41,14 @@ exports.startInterval = () => {
 | 
			
		||||
                exports.latestVersion = res.data.slow;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } catch (_) { }
 | 
			
		||||
        } catch (_) {
 | 
			
		||||
            log.info("update-checker", "Failed to check for new versions");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    check();
 | 
			
		||||
    interval = setInterval(check, 3600 * 1000 * 48);
 | 
			
		||||
    interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
 | 
			
		||||
/**
 | 
			
		||||
 * Emits the version information to the client.
 | 
			
		||||
 * @param {Socket} socket Socket.io socket instance
 | 
			
		||||
 * @param {boolean} hideVersion
 | 
			
		||||
 * @returns {Promise<void>}
 | 
			
		||||
 */
 | 
			
		||||
async function sendInfo(socket) {
 | 
			
		||||
async function sendInfo(socket, hideVersion = false) {
 | 
			
		||||
    let version;
 | 
			
		||||
    let latestVersion;
 | 
			
		||||
 | 
			
		||||
    if (!hideVersion) {
 | 
			
		||||
        version = checkVersion.version;
 | 
			
		||||
        latestVersion = checkVersion.latestVersion;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.emit("info", {
 | 
			
		||||
        version: checkVersion.version,
 | 
			
		||||
        latestVersion: checkVersion.latestVersion,
 | 
			
		||||
        version,
 | 
			
		||||
        latestVersion,
 | 
			
		||||
        primaryBaseURL: await setting("primaryBaseURL"),
 | 
			
		||||
        serverTimezone: await server.getTimezone(),
 | 
			
		||||
        serverTimezoneOffset: server.getTimezoneOffset(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
const args = require("args-parser")(process.argv);
 | 
			
		||||
// Interop with browser
 | 
			
		||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
 | 
			
		||||
const demoMode = args["demo"] || false;
 | 
			
		||||
 | 
			
		||||
const badgeConstants = {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ const { R } = require("redbean-node");
 | 
			
		||||
const { setSetting, setting } = require("./util-server");
 | 
			
		||||
const { log, sleep } = require("../src/util");
 | 
			
		||||
const knex = require("knex");
 | 
			
		||||
const { PluginsManager } = require("./plugins-manager");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database & App Data Folder
 | 
			
		||||
@@ -22,6 +21,8 @@ class Database {
 | 
			
		||||
     */
 | 
			
		||||
    static uploadDir;
 | 
			
		||||
 | 
			
		||||
    static screenshotDir;
 | 
			
		||||
 | 
			
		||||
    static path;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -70,6 +71,8 @@ class Database {
 | 
			
		||||
        "patch-monitor-tls.sql": true,
 | 
			
		||||
        "patch-maintenance-cron.sql": true,
 | 
			
		||||
        "patch-add-parent-monitor.sql": true,
 | 
			
		||||
        "patch-add-invert-keyword.sql": true,
 | 
			
		||||
        "patch-added-json-query.sql": true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -88,12 +91,6 @@ class Database {
 | 
			
		||||
        // Data Directory (must be end with "/")
 | 
			
		||||
        Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
 | 
			
		||||
 | 
			
		||||
        // Plugin feature is working only if the dataDir = "./data";
 | 
			
		||||
        if (Database.dataDir !== "./data/") {
 | 
			
		||||
            log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
 | 
			
		||||
            PluginsManager.disable = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Database.path = Database.dataDir + "kuma.db";
 | 
			
		||||
        if (! fs.existsSync(Database.dataDir)) {
 | 
			
		||||
            fs.mkdirSync(Database.dataDir, { recursive: true });
 | 
			
		||||
@@ -105,6 +102,12 @@ class Database {
 | 
			
		||||
            fs.mkdirSync(Database.uploadDir, { recursive: true });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create screenshot dir
 | 
			
		||||
        Database.screenshotDir = Database.dataDir + "screenshots/";
 | 
			
		||||
        if (! fs.existsSync(Database.screenshotDir)) {
 | 
			
		||||
            fs.mkdirSync(Database.screenshotDir, { recursive: true });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.info("db", `Data Dir: ${Database.dataDir}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -161,12 +164,12 @@ class Database {
 | 
			
		||||
            await R.exec("PRAGMA journal_mode = WAL");
 | 
			
		||||
        }
 | 
			
		||||
        await R.exec("PRAGMA cache_size = -12000");
 | 
			
		||||
        await R.exec("PRAGMA auto_vacuum = FULL");
 | 
			
		||||
        await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
 | 
			
		||||
 | 
			
		||||
        // This ensures that an operating system crash or power failure will not corrupt the database.
 | 
			
		||||
        // FULL synchronous is very safe, but it is also slower.
 | 
			
		||||
        // Read more: https://sqlite.org/pragma.html#pragma_synchronous
 | 
			
		||||
        await R.exec("PRAGMA synchronous = FULL");
 | 
			
		||||
        await R.exec("PRAGMA synchronous = NORMAL");
 | 
			
		||||
 | 
			
		||||
        if (!noLog) {
 | 
			
		||||
            log.info("db", "SQLite config:");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
const childProcess = require("child_process");
 | 
			
		||||
 | 
			
		||||
class Git {
 | 
			
		||||
 | 
			
		||||
    static clone(repoURL, cwd, targetDir = ".") {
 | 
			
		||||
        let result = childProcess.spawnSync("git", [
 | 
			
		||||
            "clone",
 | 
			
		||||
            repoURL,
 | 
			
		||||
            targetDir,
 | 
			
		||||
        ], {
 | 
			
		||||
            cwd: cwd,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (result.status !== 0) {
 | 
			
		||||
            throw new Error(result.stderr.toString("utf-8"));
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    Git,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
 | 
			
		||||
const { clearOldData } = require("./jobs/clear-old-data");
 | 
			
		||||
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
 | 
			
		||||
const Cron = require("croner");
 | 
			
		||||
 | 
			
		||||
const jobs = [
 | 
			
		||||
@@ -9,6 +10,12 @@ const jobs = [
 | 
			
		||||
        jobFunc: clearOldData,
 | 
			
		||||
        croner: null,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: "incremental-vacuum",
 | 
			
		||||
        interval: "*/5 * * * *",
 | 
			
		||||
        jobFunc: incrementalVacuum,
 | 
			
		||||
        croner: null,
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,8 @@ const clearOldData = async () => {
 | 
			
		||||
                "DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
 | 
			
		||||
                [ parsedPeriod ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            await R.exec("PRAGMA optimize;");
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.error("clearOldData", `Failed to clear old data: ${e.message}`);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								server/jobs/incremental-vacuum.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/jobs/incremental-vacuum.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
const { R } = require("redbean-node");
 | 
			
		||||
const { log } = require("../../src/util");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Run incremental_vacuum and checkpoint the WAL.
 | 
			
		||||
 * @return {Promise<void>} A promise that resolves when the process is finished.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const incrementalVacuum = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
 | 
			
		||||
        await R.exec("PRAGMA incremental_vacuum(200)");
 | 
			
		||||
        await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        log.error("incrementalVacuum", `Failed: ${e.message}`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    incrementalVacuum,
 | 
			
		||||
};
 | 
			
		||||
@@ -20,6 +20,8 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
 | 
			
		||||
const { DockerHost } = require("../docker");
 | 
			
		||||
const { UptimeCacheList } = require("../uptime-cache-list");
 | 
			
		||||
const Gamedig = require("gamedig");
 | 
			
		||||
const jsonata = require("jsonata");
 | 
			
		||||
const jwt = require("jsonwebtoken");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * status:
 | 
			
		||||
@@ -70,6 +72,12 @@ class Monitor extends BeanModel {
 | 
			
		||||
 | 
			
		||||
        const tags = await this.getTags();
 | 
			
		||||
 | 
			
		||||
        let screenshot = null;
 | 
			
		||||
 | 
			
		||||
        if (this.type === "real-browser") {
 | 
			
		||||
            screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let data = {
 | 
			
		||||
            id: this.id,
 | 
			
		||||
            name: this.name,
 | 
			
		||||
@@ -90,6 +98,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
            retryInterval: this.retryInterval,
 | 
			
		||||
            resendInterval: this.resendInterval,
 | 
			
		||||
            keyword: this.keyword,
 | 
			
		||||
            invertKeyword: this.isInvertKeyword(),
 | 
			
		||||
            expiryNotification: this.isEnabledExpiryNotification(),
 | 
			
		||||
            ignoreTls: this.getIgnoreTls(),
 | 
			
		||||
            upsideDown: this.isUpsideDown(),
 | 
			
		||||
@@ -117,7 +126,10 @@ class Monitor extends BeanModel {
 | 
			
		||||
            radiusCalledStationId: this.radiusCalledStationId,
 | 
			
		||||
            radiusCallingStationId: this.radiusCallingStationId,
 | 
			
		||||
            game: this.game,
 | 
			
		||||
            httpBodyEncoding: this.httpBodyEncoding
 | 
			
		||||
            httpBodyEncoding: this.httpBodyEncoding,
 | 
			
		||||
            jsonPath: this.jsonPath,
 | 
			
		||||
            expectedValue: this.expectedValue,
 | 
			
		||||
            screenshot,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (includeSensitiveData) {
 | 
			
		||||
@@ -199,6 +211,14 @@ class Monitor extends BeanModel {
 | 
			
		||||
        return Boolean(this.upsideDown);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse to boolean
 | 
			
		||||
     * @returns {boolean}
 | 
			
		||||
     */
 | 
			
		||||
    isInvertKeyword() {
 | 
			
		||||
        return Boolean(this.invertKeyword);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse to boolean
 | 
			
		||||
     * @returns {boolean}
 | 
			
		||||
@@ -303,7 +323,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
                        bean.msg = "Group empty";
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                } else if (this.type === "http" || this.type === "keyword") {
 | 
			
		||||
                } else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
 | 
			
		||||
                    // Do not do any queries/high loading things before the "bean.ping"
 | 
			
		||||
                    let startTime = dayjs().valueOf();
 | 
			
		||||
 | 
			
		||||
@@ -431,7 +451,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
 | 
			
		||||
                    if (this.type === "http") {
 | 
			
		||||
                        bean.status = UP;
 | 
			
		||||
                    } else {
 | 
			
		||||
                    } else if (this.type === "keyword") {
 | 
			
		||||
 | 
			
		||||
                        let data = res.data;
 | 
			
		||||
 | 
			
		||||
@@ -440,17 +460,37 @@ class Monitor extends BeanModel {
 | 
			
		||||
                            data = JSON.stringify(data);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (data.includes(this.keyword)) {
 | 
			
		||||
                            bean.msg += ", keyword is found";
 | 
			
		||||
                        let keywordFound = data.includes(this.keyword);
 | 
			
		||||
                        if (keywordFound === !this.isInvertKeyword()) {
 | 
			
		||||
                            bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
 | 
			
		||||
                            bean.status = UP;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
 | 
			
		||||
                            if (data.length > 50) {
 | 
			
		||||
                                data = data.substring(0, 47) + "...";
 | 
			
		||||
                            }
 | 
			
		||||
                            throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
 | 
			
		||||
                            throw new Error(bean.msg + ", but keyword is " +
 | 
			
		||||
                                (keywordFound ? "present" : "not") + " in [" + data + "]");
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    } else if (this.type === "json-query") {
 | 
			
		||||
                        let data = res.data;
 | 
			
		||||
 | 
			
		||||
                        // convert data to object
 | 
			
		||||
                        if (typeof data === "string") {
 | 
			
		||||
                            data = JSON.parse(data);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        let expression = jsonata(this.jsonPath);
 | 
			
		||||
 | 
			
		||||
                        let result = await expression.evaluate(data);
 | 
			
		||||
 | 
			
		||||
                        if (result.toString() === this.expectedValue) {
 | 
			
		||||
                            bean.msg += ", expected value is found";
 | 
			
		||||
                            bean.status = UP;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                } else if (this.type === "port") {
 | 
			
		||||
@@ -525,7 +565,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
                            // No need to insert successful heartbeat for push type, so end here
 | 
			
		||||
                            retries = 0;
 | 
			
		||||
                            log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
 | 
			
		||||
                            this.heartbeatInterval = setTimeout(beat, timeout);
 | 
			
		||||
                            this.heartbeatInterval = setTimeout(safeBeat, timeout);
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
@@ -618,9 +658,15 @@ class Monitor extends BeanModel {
 | 
			
		||||
 | 
			
		||||
                    log.debug("monitor", `[${this.name}] Axios Request`);
 | 
			
		||||
                    let res = await axios.request(options);
 | 
			
		||||
 | 
			
		||||
                    if (res.data.State.Running) {
 | 
			
		||||
                        bean.status = UP;
 | 
			
		||||
                        bean.msg = res.data.State.Status;
 | 
			
		||||
                        if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
 | 
			
		||||
                            bean.status = PENDING;
 | 
			
		||||
                            bean.msg = res.data.State.Health.Status;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            bean.status = UP;
 | 
			
		||||
                            bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        throw Error("Container State is " + res.data.State.Status);
 | 
			
		||||
                    }
 | 
			
		||||
@@ -649,7 +695,6 @@ class Monitor extends BeanModel {
 | 
			
		||||
                        grpcEnableTls: this.grpcEnableTls,
 | 
			
		||||
                        grpcMethod: this.grpcMethod,
 | 
			
		||||
                        grpcBody: this.grpcBody,
 | 
			
		||||
                        keyword: this.keyword
 | 
			
		||||
                    };
 | 
			
		||||
                    const response = await grpcQuery(options);
 | 
			
		||||
                    bean.ping = dayjs().valueOf() - startTime;
 | 
			
		||||
@@ -662,13 +707,14 @@ class Monitor extends BeanModel {
 | 
			
		||||
                        bean.status = DOWN;
 | 
			
		||||
                        bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (response.data.toString().includes(this.keyword)) {
 | 
			
		||||
                        let keywordFound = response.data.toString().includes(this.keyword);
 | 
			
		||||
                        if (keywordFound === !this.isInvertKeyword()) {
 | 
			
		||||
                            bean.status = UP;
 | 
			
		||||
                            bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
 | 
			
		||||
                            bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
 | 
			
		||||
                            log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
 | 
			
		||||
                            bean.status = DOWN;
 | 
			
		||||
                            bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
 | 
			
		||||
                            bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (this.type === "postgres") {
 | 
			
		||||
@@ -715,7 +761,8 @@ class Monitor extends BeanModel {
 | 
			
		||||
                            this.radiusCalledStationId,
 | 
			
		||||
                            this.radiusCallingStationId,
 | 
			
		||||
                            this.radiusSecret,
 | 
			
		||||
                            port
 | 
			
		||||
                            port,
 | 
			
		||||
                            this.interval * 1000 * 0.8,
 | 
			
		||||
                        );
 | 
			
		||||
                        if (resp.code) {
 | 
			
		||||
                            bean.msg = resp.code;
 | 
			
		||||
@@ -740,7 +787,7 @@ class Monitor extends BeanModel {
 | 
			
		||||
                } else if (this.type in UptimeKumaServer.monitorTypeList) {
 | 
			
		||||
                    let startTime = dayjs().valueOf();
 | 
			
		||||
                    const monitorType = UptimeKumaServer.monitorTypeList[this.type];
 | 
			
		||||
                    await monitorType.check(this, bean);
 | 
			
		||||
                    await monitorType.check(this, bean, UptimeKumaServer.getInstance());
 | 
			
		||||
                    if (!bean.ping) {
 | 
			
		||||
                        bean.ping = dayjs().valueOf() - startTime;
 | 
			
		||||
                    }
 | 
			
		||||
@@ -1463,6 +1510,17 @@ class Monitor extends BeanModel {
 | 
			
		||||
        return childrenIDs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unlinks all children of the the group monitor
 | 
			
		||||
     * @param {number} groupID ID of group to remove children of
 | 
			
		||||
     * @returns {Promise<void>}
 | 
			
		||||
     */
 | 
			
		||||
    static async unlinkAllChildren(groupID) {
 | 
			
		||||
        return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
 | 
			
		||||
            null, groupID
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
	 * Checks recursive if parent (ancestors) are active
 | 
			
		||||
	 * @param {number} monitorID ID of the monitor to get
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,10 @@ class MonitorType {
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Monitor} monitor
 | 
			
		||||
     * @param {Heartbeat} heartbeat
 | 
			
		||||
     * @param {UptimeKumaServer} server
 | 
			
		||||
     * @returns {Promise<void>}
 | 
			
		||||
     */
 | 
			
		||||
    async check(monitor, heartbeat) {
 | 
			
		||||
    async check(monitor, heartbeat, server) {
 | 
			
		||||
        throw new Error("You need to override check()");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										212
									
								
								server/monitor-types/real-browser-monitor-type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								server/monitor-types/real-browser-monitor-type.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
			
		||||
const { MonitorType } = require("./monitor-type");
 | 
			
		||||
const { chromium } = require("playwright-core");
 | 
			
		||||
const { UP, log } = require("../../src/util");
 | 
			
		||||
const { Settings } = require("../settings");
 | 
			
		||||
const commandExistsSync = require("command-exists").sync;
 | 
			
		||||
const childProcess = require("child_process");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const Database = require("../database");
 | 
			
		||||
const jwt = require("jsonwebtoken");
 | 
			
		||||
const config = require("../config");
 | 
			
		||||
 | 
			
		||||
let browser = null;
 | 
			
		||||
 | 
			
		||||
let allowedList = [];
 | 
			
		||||
let lastAutoDetectChromeExecutable = null;
 | 
			
		||||
 | 
			
		||||
if (process.platform === "win32") {
 | 
			
		||||
    allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
 | 
			
		||||
    allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
 | 
			
		||||
    allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
 | 
			
		||||
 | 
			
		||||
    // Allow Chromium too
 | 
			
		||||
    allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
 | 
			
		||||
    allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
 | 
			
		||||
    allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
 | 
			
		||||
 | 
			
		||||
    // For Loop A to Z
 | 
			
		||||
    for (let i = 65; i <= 90; i++) {
 | 
			
		||||
        let drive = String.fromCharCode(i);
 | 
			
		||||
        allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
 | 
			
		||||
        allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
} else if (process.platform === "linux") {
 | 
			
		||||
    allowedList = [
 | 
			
		||||
        "chromium",
 | 
			
		||||
        "chromium-browser",
 | 
			
		||||
        "google-chrome",
 | 
			
		||||
 | 
			
		||||
        "/usr/bin/chromium",
 | 
			
		||||
        "/usr/bin/chromium-browser",
 | 
			
		||||
        "/usr/bin/google-chrome",
 | 
			
		||||
    ];
 | 
			
		||||
} else if (process.platform === "darwin") {
 | 
			
		||||
    // TODO: Generated by GitHub Copilot, but not sure if it's correct
 | 
			
		||||
    allowedList = [
 | 
			
		||||
        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
 | 
			
		||||
        "/Applications/Chromium.app/Contents/MacOS/Chromium",
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
log.debug("chrome", allowedList);
 | 
			
		||||
 | 
			
		||||
async function isAllowedChromeExecutable(executablePath) {
 | 
			
		||||
    console.log(config.args);
 | 
			
		||||
    if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if the executablePath is in the list of allowed executables
 | 
			
		||||
    return allowedList.includes(executablePath);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getBrowser() {
 | 
			
		||||
    if (!browser) {
 | 
			
		||||
        let executablePath = await Settings.get("chromeExecutable");
 | 
			
		||||
 | 
			
		||||
        executablePath = await prepareChromeExecutable(executablePath);
 | 
			
		||||
 | 
			
		||||
        browser = await chromium.launch({
 | 
			
		||||
            //headless: false,
 | 
			
		||||
            executablePath,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return browser;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function prepareChromeExecutable(executablePath) {
 | 
			
		||||
    // Special code for using the playwright_chromium
 | 
			
		||||
    if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
 | 
			
		||||
        // Set to undefined = use playwright_chromium
 | 
			
		||||
        executablePath = undefined;
 | 
			
		||||
    } else if (!executablePath) {
 | 
			
		||||
        if (process.env.UPTIME_KUMA_IS_CONTAINER) {
 | 
			
		||||
            executablePath = "/usr/bin/chromium";
 | 
			
		||||
 | 
			
		||||
            // Install chromium in container via apt install
 | 
			
		||||
            if ( !commandExistsSync(executablePath)) {
 | 
			
		||||
                await new Promise((resolve, reject) => {
 | 
			
		||||
                    log.info("Chromium", "Installing Chromium...");
 | 
			
		||||
                    let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
 | 
			
		||||
 | 
			
		||||
                    // On exit
 | 
			
		||||
                    child.on("exit", (code) => {
 | 
			
		||||
                        log.info("Chromium", "apt install chromium exited with code " + code);
 | 
			
		||||
 | 
			
		||||
                        if (code === 0) {
 | 
			
		||||
                            log.info("Chromium", "Installed Chromium");
 | 
			
		||||
                            let version = childProcess.execSync(executablePath + " --version").toString("utf8");
 | 
			
		||||
                            log.info("Chromium", "Chromium version: " + version);
 | 
			
		||||
                            resolve();
 | 
			
		||||
                        } else if (code === 100) {
 | 
			
		||||
                            reject(new Error("Installing Chromium, please wait..."));
 | 
			
		||||
                        } else {
 | 
			
		||||
                            reject(new Error("apt install chromium failed with code " + code));
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            executablePath = findChrome(allowedList);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        // User specified a path
 | 
			
		||||
        // Check if the executablePath is in the list of allowed
 | 
			
		||||
        if (!await isAllowedChromeExecutable(executablePath)) {
 | 
			
		||||
            throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return executablePath;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function findChrome(executables) {
 | 
			
		||||
    // Use the last working executable, so we don't have to search for it again
 | 
			
		||||
    if (lastAutoDetectChromeExecutable) {
 | 
			
		||||
        if (commandExistsSync(lastAutoDetectChromeExecutable)) {
 | 
			
		||||
            return lastAutoDetectChromeExecutable;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let executable of executables) {
 | 
			
		||||
        if (commandExistsSync(executable)) {
 | 
			
		||||
            lastAutoDetectChromeExecutable = executable;
 | 
			
		||||
            return executable;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function resetChrome() {
 | 
			
		||||
    if (browser) {
 | 
			
		||||
        await browser.close();
 | 
			
		||||
        browser = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test if the chrome executable is valid and return the version
 | 
			
		||||
 * @param executablePath
 | 
			
		||||
 * @returns {Promise<string>}
 | 
			
		||||
 */
 | 
			
		||||
async function testChrome(executablePath) {
 | 
			
		||||
    try {
 | 
			
		||||
        executablePath = await prepareChromeExecutable(executablePath);
 | 
			
		||||
 | 
			
		||||
        log.info("Chromium", "Testing Chromium executable: " + executablePath);
 | 
			
		||||
 | 
			
		||||
        const browser = await chromium.launch({
 | 
			
		||||
            executablePath,
 | 
			
		||||
        });
 | 
			
		||||
        const version = browser.version();
 | 
			
		||||
        await browser.close();
 | 
			
		||||
        return version;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        throw new Error(e.message);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
class RealBrowserMonitorType extends MonitorType {
 | 
			
		||||
 | 
			
		||||
    name = "real-browser";
 | 
			
		||||
 | 
			
		||||
    async check(monitor, heartbeat, server) {
 | 
			
		||||
        const browser = await getBrowser();
 | 
			
		||||
        const context = await browser.newContext();
 | 
			
		||||
        const page = await context.newPage();
 | 
			
		||||
 | 
			
		||||
        const res = await page.goto(monitor.url, {
 | 
			
		||||
            waitUntil: "networkidle",
 | 
			
		||||
            timeout: monitor.interval * 1000 * 0.8,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
 | 
			
		||||
 | 
			
		||||
        await page.screenshot({
 | 
			
		||||
            path: path.join(Database.screenshotDir, filename),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await context.close();
 | 
			
		||||
 | 
			
		||||
        if (res.status() >= 200 && res.status() < 400) {
 | 
			
		||||
            heartbeat.status = UP;
 | 
			
		||||
            heartbeat.msg = res.status();
 | 
			
		||||
 | 
			
		||||
            const timing = res.request().timing();
 | 
			
		||||
            heartbeat.ping = timing.responseEnd;
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error(res.status() + "");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    RealBrowserMonitorType,
 | 
			
		||||
    testChrome,
 | 
			
		||||
    resetChrome,
 | 
			
		||||
};
 | 
			
		||||
@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post(
 | 
			
		||||
                `${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
 | 
			
		||||
                `${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
 | 
			
		||||
                {
 | 
			
		||||
                    title: "Uptime Kuma",
 | 
			
		||||
                    message,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
 | 
			
		||||
 | 
			
		||||
    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
 | 
			
		||||
        let okMsg = "Sent Successfully.";
 | 
			
		||||
 | 
			
		||||
        if (notification.slackchannelnotify) {
 | 
			
		||||
            msg += " <!channel>";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (heartbeatJSON == null) {
 | 
			
		||||
                let data = {
 | 
			
		||||
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
 | 
			
		||||
                                "type": "header",
 | 
			
		||||
                                "text": {
 | 
			
		||||
                                    "type": "plain_text",
 | 
			
		||||
                                    "text": "Uptime Kuma Alert",
 | 
			
		||||
                                    "text": textMsg,
 | 
			
		||||
                                },
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								server/notification-providers/smsc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/notification-providers/smsc.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
const NotificationProvider = require("./notification-provider");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
 | 
			
		||||
class SMSC extends NotificationProvider {
 | 
			
		||||
    name = "smsc";
 | 
			
		||||
 | 
			
		||||
    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
 | 
			
		||||
        let okMsg = "Sent Successfully.";
 | 
			
		||||
        try {
 | 
			
		||||
            let config = {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    "Content-Type": "application/json",
 | 
			
		||||
                    "Accept": "text/json",
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let getArray = [
 | 
			
		||||
                "fmt=3",
 | 
			
		||||
                "translit=" + notification.smscTranslit,
 | 
			
		||||
                "login=" + notification.smscLogin,
 | 
			
		||||
                "psw=" + notification.smscPassword,
 | 
			
		||||
                "phones=" + notification.smscToNumber,
 | 
			
		||||
                "mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
 | 
			
		||||
            ];
 | 
			
		||||
            if (notification.smscSenderName !== "") {
 | 
			
		||||
                getArray.push("sender=" + notification.smscSenderName);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
 | 
			
		||||
            if (resp.data.id === undefined) {
 | 
			
		||||
                let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
 | 
			
		||||
                this.throwGeneralAxiosError(error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return okMsg;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.throwGeneralAxiosError(error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = SMSC;
 | 
			
		||||
@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
 | 
			
		||||
                if (monitorJSON !== null) {
 | 
			
		||||
                    monitorName = monitorJSON["name"];
 | 
			
		||||
 | 
			
		||||
                    if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
 | 
			
		||||
                    if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
 | 
			
		||||
                        monitorHostnameOrURL = monitorJSON["url"];
 | 
			
		||||
                    } else {
 | 
			
		||||
                        monitorHostnameOrURL = monitorJSON["hostname"];
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
 | 
			
		||||
        let okMsg = "Sent Successfully.";
 | 
			
		||||
 | 
			
		||||
        let accountSID = notification.twilioAccountSID;
 | 
			
		||||
        let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
 | 
			
		||||
        let authToken = notification.twilioAuthToken;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
@@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
 | 
			
		||||
            let config = {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
 | 
			
		||||
                    "Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
 | 
			
		||||
                    "Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
const NotificationProvider = require("./notification-provider");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const FormData = require("form-data");
 | 
			
		||||
const { Liquid } = require("liquidjs");
 | 
			
		||||
 | 
			
		||||
class Webhook extends NotificationProvider {
 | 
			
		||||
 | 
			
		||||
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
 | 
			
		||||
                monitor: monitorJSON,
 | 
			
		||||
                msg,
 | 
			
		||||
            };
 | 
			
		||||
            let finalData;
 | 
			
		||||
            let config = {
 | 
			
		||||
                headers: {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (notification.webhookContentType === "form-data") {
 | 
			
		||||
                finalData = new FormData();
 | 
			
		||||
                finalData.append("data", JSON.stringify(data));
 | 
			
		||||
                config.headers = finalData.getHeaders();
 | 
			
		||||
            } else {
 | 
			
		||||
                finalData = data;
 | 
			
		||||
                const formData = new FormData();
 | 
			
		||||
                formData.append("data", JSON.stringify(data));
 | 
			
		||||
                config.headers = formData.getHeaders();
 | 
			
		||||
                data = formData;
 | 
			
		||||
            } else if (notification.webhookContentType === "custom") {
 | 
			
		||||
                // Initialize LiquidJS and parse the custom Body Template
 | 
			
		||||
                const engine = new Liquid();
 | 
			
		||||
                const tpl = engine.parse(notification.webhookCustomBody);
 | 
			
		||||
 | 
			
		||||
                // Insert templated values into Body
 | 
			
		||||
                data = await engine.render(tpl,
 | 
			
		||||
                    {
 | 
			
		||||
                        msg,
 | 
			
		||||
                        heartbeatJSON,
 | 
			
		||||
                        monitorJSON
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (notification.webhookAdditionalHeaders) {
 | 
			
		||||
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await axios.post(notification.webhookURL, finalData, config);
 | 
			
		||||
            await axios.post(notification.webhookURL, data, config);
 | 
			
		||||
            return okMsg;
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
 | 
			
		||||
const Apprise = require("./notification-providers/apprise");
 | 
			
		||||
const Bark = require("./notification-providers/bark");
 | 
			
		||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
 | 
			
		||||
const SMSC = require("./notification-providers/smsc");
 | 
			
		||||
const DingDing = require("./notification-providers/dingding");
 | 
			
		||||
const Discord = require("./notification-providers/discord");
 | 
			
		||||
const Feishu = require("./notification-providers/feishu");
 | 
			
		||||
@@ -68,6 +69,7 @@ class Notification {
 | 
			
		||||
            new Apprise(),
 | 
			
		||||
            new Bark(),
 | 
			
		||||
            new ClickSendSMS(),
 | 
			
		||||
            new SMSC(),
 | 
			
		||||
            new DingDing(),
 | 
			
		||||
            new Discord(),
 | 
			
		||||
            new Feishu(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
class Plugin {
 | 
			
		||||
    async load() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async unload() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    Plugin,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,256 +0,0 @@
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
const { log } = require("../src/util");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const { Git } = require("./git");
 | 
			
		||||
const childProcess = require("child_process");
 | 
			
		||||
 | 
			
		||||
class PluginsManager {
 | 
			
		||||
 | 
			
		||||
    static disable = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin List
 | 
			
		||||
     * @type {PluginWrapper[]}
 | 
			
		||||
     */
 | 
			
		||||
    pluginList = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugins Dir
 | 
			
		||||
     */
 | 
			
		||||
    pluginsDir;
 | 
			
		||||
 | 
			
		||||
    server;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {UptimeKumaServer} server
 | 
			
		||||
     */
 | 
			
		||||
    constructor(server) {
 | 
			
		||||
        this.server = server;
 | 
			
		||||
 | 
			
		||||
        if (!PluginsManager.disable) {
 | 
			
		||||
            this.pluginsDir = "./data/plugins/";
 | 
			
		||||
 | 
			
		||||
            if (! fs.existsSync(this.pluginsDir)) {
 | 
			
		||||
                fs.mkdirSync(this.pluginsDir, { recursive: true });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            log.debug("plugin", "Scanning plugin directory");
 | 
			
		||||
            let list = fs.readdirSync(this.pluginsDir);
 | 
			
		||||
 | 
			
		||||
            this.pluginList = [];
 | 
			
		||||
            for (let item of list) {
 | 
			
		||||
                this.loadPlugin(item);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            log.warn("PLUGIN", "Skip scanning plugin directory");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Install a Plugin
 | 
			
		||||
     */
 | 
			
		||||
    async loadPlugin(name) {
 | 
			
		||||
        log.info("plugin", "Load " + name);
 | 
			
		||||
        let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await plugin.load();
 | 
			
		||||
            this.pluginList.push(plugin);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
 | 
			
		||||
            log.error("plugin", "Reason: " + e.message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download a Plugin
 | 
			
		||||
     * @param {string} repoURL Git repo url
 | 
			
		||||
     * @param {string} name Directory name, also known as plugin unique name
 | 
			
		||||
     */
 | 
			
		||||
    downloadPlugin(repoURL, name) {
 | 
			
		||||
        if (fs.existsSync(this.pluginsDir + name)) {
 | 
			
		||||
            log.info("plugin", "Plugin folder already exists? Removing...");
 | 
			
		||||
            fs.rmSync(this.pluginsDir + name, {
 | 
			
		||||
                recursive: true
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        log.info("plugin", "Installing plugin: " + name + " " + repoURL);
 | 
			
		||||
        let result = Git.clone(repoURL, this.pluginsDir, name);
 | 
			
		||||
        log.info("plugin", "Install result: " + result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a plugin
 | 
			
		||||
     * @param {string} name
 | 
			
		||||
     */
 | 
			
		||||
    async removePlugin(name) {
 | 
			
		||||
        log.info("plugin", "Removing plugin: " + name);
 | 
			
		||||
        for (let plugin of this.pluginList) {
 | 
			
		||||
            if (plugin.info.name === name) {
 | 
			
		||||
                await plugin.unload();
 | 
			
		||||
 | 
			
		||||
                // Delete the plugin directory
 | 
			
		||||
                fs.rmSync(this.pluginsDir + name, {
 | 
			
		||||
                    recursive: true
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("plugin", "Plugin not found: " + name);
 | 
			
		||||
        throw new Error("Plugin not found: " + name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * TODO: Update a plugin
 | 
			
		||||
     * Only available for plugins which were downloaded from the official list
 | 
			
		||||
     * @param pluginID
 | 
			
		||||
     */
 | 
			
		||||
    updatePlugin(pluginID) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the plugin list from server + local installed plugin list
 | 
			
		||||
     * Item will be merged if the `name` is the same.
 | 
			
		||||
     * @returns {Promise<[]>}
 | 
			
		||||
     */
 | 
			
		||||
    async fetchPluginList() {
 | 
			
		||||
        let remotePluginList;
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
 | 
			
		||||
            remotePluginList = res.data.pluginList;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.error("plugin", "Failed to fetch plugin list: " + e.message);
 | 
			
		||||
            remotePluginList = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let plugin of this.pluginList) {
 | 
			
		||||
            let find = false;
 | 
			
		||||
            // Try to merge
 | 
			
		||||
            for (let remotePlugin of remotePluginList) {
 | 
			
		||||
                if (remotePlugin.name === plugin.info.name) {
 | 
			
		||||
                    find = true;
 | 
			
		||||
                    remotePlugin.installed = true;
 | 
			
		||||
                    remotePlugin.name = plugin.info.name;
 | 
			
		||||
                    remotePlugin.fullName = plugin.info.fullName;
 | 
			
		||||
                    remotePlugin.description = plugin.info.description;
 | 
			
		||||
                    remotePlugin.version = plugin.info.version;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Local plugin
 | 
			
		||||
            if (!find) {
 | 
			
		||||
                plugin.info.local = true;
 | 
			
		||||
                remotePluginList.push(plugin.info);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sort Installed first, then sort by name
 | 
			
		||||
        return remotePluginList.sort((a, b) => {
 | 
			
		||||
            if (a.installed === b.installed) {
 | 
			
		||||
                if (a.fullName < b.fullName) {
 | 
			
		||||
                    return -1;
 | 
			
		||||
                }
 | 
			
		||||
                if (a.fullName > b.fullName) {
 | 
			
		||||
                    return 1;
 | 
			
		||||
                }
 | 
			
		||||
                return 0;
 | 
			
		||||
            } else if (a.installed) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            } else {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PluginWrapper {
 | 
			
		||||
 | 
			
		||||
    server = undefined;
 | 
			
		||||
    pluginDir = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Must be an `new-able` class.
 | 
			
		||||
     * @type {function}
 | 
			
		||||
     */
 | 
			
		||||
    pluginClass = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @type {Plugin}
 | 
			
		||||
     */
 | 
			
		||||
    object = undefined;
 | 
			
		||||
    info = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {UptimeKumaServer} server
 | 
			
		||||
     * @param {string} pluginDir
 | 
			
		||||
     */
 | 
			
		||||
    constructor(server, pluginDir) {
 | 
			
		||||
        this.server = server;
 | 
			
		||||
        this.pluginDir = pluginDir;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async load() {
 | 
			
		||||
        let indexFile = this.pluginDir + "/index.js";
 | 
			
		||||
        let packageJSON = this.pluginDir + "/package.json";
 | 
			
		||||
 | 
			
		||||
        log.info("plugin", "Installing dependencies");
 | 
			
		||||
 | 
			
		||||
        if (fs.existsSync(indexFile)) {
 | 
			
		||||
            // Install dependencies
 | 
			
		||||
            let result = childProcess.spawnSync("npm", [ "install" ], {
 | 
			
		||||
                cwd: this.pluginDir,
 | 
			
		||||
                env: {
 | 
			
		||||
                    ...process.env,
 | 
			
		||||
                    PLAYWRIGHT_BROWSERS_PATH: "../../browsers",    // Special handling for read-browser-monitor
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (result.stdout) {
 | 
			
		||||
                log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
 | 
			
		||||
            } else {
 | 
			
		||||
                log.warn("plugin", "Install dependencies result: no output");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.pluginClass = require(path.join(process.cwd(), indexFile));
 | 
			
		||||
 | 
			
		||||
            let pluginClassType = typeof this.pluginClass;
 | 
			
		||||
 | 
			
		||||
            if (pluginClassType === "function") {
 | 
			
		||||
                this.object = new this.pluginClass(this.server);
 | 
			
		||||
                await this.object.load();
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error("Invalid plugin, it does not export a class");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fs.existsSync(packageJSON)) {
 | 
			
		||||
                this.info = require(path.join(process.cwd(), packageJSON));
 | 
			
		||||
            } else {
 | 
			
		||||
                this.info.fullName = this.pluginDir;
 | 
			
		||||
                this.info.name = "[unknown]";
 | 
			
		||||
                this.info.version = "[unknown-version]";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.info.installed = true;
 | 
			
		||||
            log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async unload() {
 | 
			
		||||
        await this.object.unload();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    PluginsManager,
 | 
			
		||||
    PluginWrapper
 | 
			
		||||
};
 | 
			
		||||
@@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
 | 
			
		||||
                if (!tlsInfo.valid) {
 | 
			
		||||
                    // return a "Bad Cert" badge in naColor (grey), when cert is not valid
 | 
			
		||||
                    badgeValues.message = "Bad Cert";
 | 
			
		||||
                    badgeValues.color = badgeConstants.downColor;
 | 
			
		||||
                    badgeValues.color = downColor;
 | 
			
		||||
                } else {
 | 
			
		||||
                    const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
 | 
			
		||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
 | 
			
		||||
const { R } = require("redbean-node");
 | 
			
		||||
const Monitor = require("../model/monitor");
 | 
			
		||||
const { badgeConstants } = require("../config");
 | 
			
		||||
const { makeBadge } = require("badge-maker");
 | 
			
		||||
 | 
			
		||||
let router = express.Router();
 | 
			
		||||
 | 
			
		||||
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// overall status-page status badge
 | 
			
		||||
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
 | 
			
		||||
    allowDevAllOrigin(response);
 | 
			
		||||
    const slug = request.params.slug;
 | 
			
		||||
    const statusPageID = await StatusPage.slugToID(slug);
 | 
			
		||||
    const {
 | 
			
		||||
        label,
 | 
			
		||||
        upColor = badgeConstants.defaultUpColor,
 | 
			
		||||
        downColor = badgeConstants.defaultDownColor,
 | 
			
		||||
        partialColor = "#F6BE00",
 | 
			
		||||
        maintenanceColor = "#808080",
 | 
			
		||||
        style = badgeConstants.defaultStyle
 | 
			
		||||
    } = request.query;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        let monitorIDList = await R.getCol(`
 | 
			
		||||
            SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
 | 
			
		||||
            WHERE monitor_group.group_id = \`group\`.id
 | 
			
		||||
            AND public = 1
 | 
			
		||||
            AND \`group\`.status_page_id = ?
 | 
			
		||||
        `, [
 | 
			
		||||
            statusPageID
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        let hasUp = false;
 | 
			
		||||
        let hasDown = false;
 | 
			
		||||
        let hasMaintenance = false;
 | 
			
		||||
 | 
			
		||||
        for (let monitorID of monitorIDList) {
 | 
			
		||||
            // retrieve the latest heartbeat
 | 
			
		||||
            let beat = await R.getAll(`
 | 
			
		||||
                    SELECT * FROM heartbeat
 | 
			
		||||
                    WHERE monitor_id = ?
 | 
			
		||||
                    ORDER BY time DESC
 | 
			
		||||
                    LIMIT 1
 | 
			
		||||
            `, [
 | 
			
		||||
                monitorID,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // to be sure, when corresponding monitor not found
 | 
			
		||||
            if (beat.length === 0) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // handle status of beat
 | 
			
		||||
            if (beat[0].status === 3) {
 | 
			
		||||
                hasMaintenance = true;
 | 
			
		||||
            } else if (beat[0].status === 2) {
 | 
			
		||||
                // ignored
 | 
			
		||||
            } else if (beat[0].status === 1) {
 | 
			
		||||
                hasUp = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                hasDown = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const badgeValues = { style };
 | 
			
		||||
 | 
			
		||||
        if (!hasUp && !hasDown && !hasMaintenance) {
 | 
			
		||||
            // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
 | 
			
		||||
 | 
			
		||||
            badgeValues.message = "N/A";
 | 
			
		||||
            badgeValues.color = badgeConstants.naColor;
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            if (hasMaintenance) {
 | 
			
		||||
                badgeValues.label = label ? label : "";
 | 
			
		||||
                badgeValues.color = maintenanceColor;
 | 
			
		||||
                badgeValues.message = "Maintenance";
 | 
			
		||||
            } else if (hasUp && !hasDown) {
 | 
			
		||||
                badgeValues.label = label ? label : "";
 | 
			
		||||
                badgeValues.color = upColor;
 | 
			
		||||
                badgeValues.message = "Up";
 | 
			
		||||
            } else if (hasUp && hasDown) {
 | 
			
		||||
                badgeValues.label = label ? label : "";
 | 
			
		||||
                badgeValues.color = partialColor;
 | 
			
		||||
                badgeValues.message = "Degraded";
 | 
			
		||||
            } else {
 | 
			
		||||
                badgeValues.label = label ? label : "";
 | 
			
		||||
                badgeValues.color = downColor;
 | 
			
		||||
                badgeValues.message = "Down";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // build the svg based on given values
 | 
			
		||||
        const svg = makeBadge(badgeValues);
 | 
			
		||||
 | 
			
		||||
        response.type("image/svg+xml");
 | 
			
		||||
        response.send(svg);
 | 
			
		||||
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        sendHttpError(response, error.message);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
 
 | 
			
		||||
@@ -147,8 +147,8 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
 | 
			
		||||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 | 
			
		||||
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
 | 
			
		||||
const apicache = require("./modules/apicache");
 | 
			
		||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
 | 
			
		||||
 | 
			
		||||
app.use(express.json());
 | 
			
		||||
 | 
			
		||||
@@ -161,12 +161,6 @@ app.use(function (req, res, next) {
 | 
			
		||||
    next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Use for decode the auth object
 | 
			
		||||
 * @type {null}
 | 
			
		||||
 */
 | 
			
		||||
let jwtSecret = null;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Show Setup Page
 | 
			
		||||
 * @type {boolean}
 | 
			
		||||
@@ -177,7 +171,6 @@ let needSetup = false;
 | 
			
		||||
    Database.init(args);
 | 
			
		||||
    await initDatabase(testMode);
 | 
			
		||||
    await server.initAfterDatabaseReady();
 | 
			
		||||
    server.loadPlugins();
 | 
			
		||||
    server.entryPage = await Settings.get("entryPage");
 | 
			
		||||
    await StatusPage.loadDomainMappingList();
 | 
			
		||||
 | 
			
		||||
@@ -215,6 +208,7 @@ let needSetup = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (isDev) {
 | 
			
		||||
        app.use(express.urlencoded({ extended: true }));
 | 
			
		||||
        app.post("/test-webhook", async (request, response) => {
 | 
			
		||||
            log.debug("test", request.headers);
 | 
			
		||||
            log.debug("test", request.body);
 | 
			
		||||
@@ -269,7 +263,7 @@ let needSetup = false;
 | 
			
		||||
    log.info("server", "Adding socket handler");
 | 
			
		||||
    io.on("connection", async (socket) => {
 | 
			
		||||
 | 
			
		||||
        sendInfo(socket);
 | 
			
		||||
        sendInfo(socket, true);
 | 
			
		||||
 | 
			
		||||
        if (needSetup) {
 | 
			
		||||
            log.info("server", "Redirect to setup page");
 | 
			
		||||
@@ -286,7 +280,7 @@ let needSetup = false;
 | 
			
		||||
            log.info("auth", `Login by token. IP=${clientIP}`);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                let decoded = jwt.verify(token, jwtSecret);
 | 
			
		||||
                let decoded = jwt.verify(token, server.jwtSecret);
 | 
			
		||||
 | 
			
		||||
                log.info("auth", "Username from JWT: " + decoded.username);
 | 
			
		||||
 | 
			
		||||
@@ -357,7 +351,7 @@ let needSetup = false;
 | 
			
		||||
                        ok: true,
 | 
			
		||||
                        token: jwt.sign({
 | 
			
		||||
                            username: data.username,
 | 
			
		||||
                        }, jwtSecret),
 | 
			
		||||
                        }, server.jwtSecret),
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -387,7 +381,7 @@ let needSetup = false;
 | 
			
		||||
                            ok: true,
 | 
			
		||||
                            token: jwt.sign({
 | 
			
		||||
                                username: data.username,
 | 
			
		||||
                            }, jwtSecret),
 | 
			
		||||
                            }, server.jwtSecret),
 | 
			
		||||
                        });
 | 
			
		||||
                    } else {
 | 
			
		||||
 | 
			
		||||
@@ -676,6 +670,7 @@ let needSetup = false;
 | 
			
		||||
        // Edit a monitor
 | 
			
		||||
        socket.on("editMonitor", async (monitor, callback) => {
 | 
			
		||||
            try {
 | 
			
		||||
                let removeGroupChildren = false;
 | 
			
		||||
                checkLogin(socket);
 | 
			
		||||
 | 
			
		||||
                let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
 | 
			
		||||
@@ -684,7 +679,7 @@ let needSetup = false;
 | 
			
		||||
                    throw new Error("Permission denied.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Check if Parent is Decendant (would cause endless loop)
 | 
			
		||||
                // Check if Parent is Descendant (would cause endless loop)
 | 
			
		||||
                if (monitor.parent !== null) {
 | 
			
		||||
                    const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
 | 
			
		||||
                    if (childIDs.includes(monitor.parent)) {
 | 
			
		||||
@@ -692,6 +687,11 @@ let needSetup = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Remove children if monitor type has changed (from group to non-group)
 | 
			
		||||
                if (bean.type === "group" && monitor.type !== bean.type) {
 | 
			
		||||
                    removeGroupChildren = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                bean.name = monitor.name;
 | 
			
		||||
                bean.description = monitor.description;
 | 
			
		||||
                bean.parent = monitor.parent;
 | 
			
		||||
@@ -713,6 +713,7 @@ let needSetup = false;
 | 
			
		||||
                bean.maxretries = monitor.maxretries;
 | 
			
		||||
                bean.port = parseInt(monitor.port);
 | 
			
		||||
                bean.keyword = monitor.keyword;
 | 
			
		||||
                bean.invertKeyword = monitor.invertKeyword;
 | 
			
		||||
                bean.ignoreTls = monitor.ignoreTls;
 | 
			
		||||
                bean.expiryNotification = monitor.expiryNotification;
 | 
			
		||||
                bean.upsideDown = monitor.upsideDown;
 | 
			
		||||
@@ -747,11 +748,17 @@ let needSetup = false;
 | 
			
		||||
                bean.radiusCallingStationId = monitor.radiusCallingStationId;
 | 
			
		||||
                bean.radiusSecret = monitor.radiusSecret;
 | 
			
		||||
                bean.httpBodyEncoding = monitor.httpBodyEncoding;
 | 
			
		||||
                bean.expectedValue = monitor.expectedValue;
 | 
			
		||||
                bean.jsonPath = monitor.jsonPath;
 | 
			
		||||
 | 
			
		||||
                bean.validate();
 | 
			
		||||
 | 
			
		||||
                await R.store(bean);
 | 
			
		||||
 | 
			
		||||
                if (removeGroupChildren) {
 | 
			
		||||
                    await Monitor.unlinkAllChildren(monitor.id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await updateMonitorNotification(bean.id, monitor.notificationIDList);
 | 
			
		||||
 | 
			
		||||
                if (bean.isActive()) {
 | 
			
		||||
@@ -897,6 +904,8 @@ let needSetup = false;
 | 
			
		||||
                    delete server.monitorList[monitorID];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const startTime = Date.now();
 | 
			
		||||
 | 
			
		||||
                await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
 | 
			
		||||
                    monitorID,
 | 
			
		||||
                    socket.userID,
 | 
			
		||||
@@ -905,6 +914,10 @@ let needSetup = false;
 | 
			
		||||
                // Fix #2880
 | 
			
		||||
                apicache.clear();
 | 
			
		||||
 | 
			
		||||
                const endTime = Date.now();
 | 
			
		||||
 | 
			
		||||
                log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
 | 
			
		||||
 | 
			
		||||
                callback({
 | 
			
		||||
                    ok: true,
 | 
			
		||||
                    msg: "Deleted Successfully.",
 | 
			
		||||
@@ -1148,6 +1161,8 @@ let needSetup = false;
 | 
			
		||||
                    await doubleCheckPassword(socket, currentPassword);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const previousChromeExecutable = await Settings.get("chromeExecutable");
 | 
			
		||||
 | 
			
		||||
                await setSettings("general", data);
 | 
			
		||||
                server.entryPage = data.entryPage;
 | 
			
		||||
 | 
			
		||||
@@ -1158,6 +1173,12 @@ let needSetup = false;
 | 
			
		||||
                    await server.setTimezone(data.serverTimezone);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // If Chrome Executable is changed, need to reset the browser
 | 
			
		||||
                if (previousChromeExecutable !== data.chromeExecutable) {
 | 
			
		||||
                    log.info("settings", "Chrome executable is changed. Resetting Chrome...");
 | 
			
		||||
                    await resetChrome();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                callback({
 | 
			
		||||
                    ok: true,
 | 
			
		||||
                    msg: "Saved"
 | 
			
		||||
@@ -1359,13 +1380,14 @@ let needSetup = false;
 | 
			
		||||
                                maxretries: monitorListData[i].maxretries,
 | 
			
		||||
                                port: monitorListData[i].port,
 | 
			
		||||
                                keyword: monitorListData[i].keyword,
 | 
			
		||||
                                invertKeyword: monitorListData[i].invertKeyword,
 | 
			
		||||
                                ignoreTls: monitorListData[i].ignoreTls,
 | 
			
		||||
                                upsideDown: monitorListData[i].upsideDown,
 | 
			
		||||
                                maxredirects: monitorListData[i].maxredirects,
 | 
			
		||||
                                accepted_statuscodes: monitorListData[i].accepted_statuscodes,
 | 
			
		||||
                                dns_resolve_type: monitorListData[i].dns_resolve_type,
 | 
			
		||||
                                dns_resolve_server: monitorListData[i].dns_resolve_server,
 | 
			
		||||
                                notificationIDList: {},
 | 
			
		||||
                                notificationIDList: monitorListData[i].notificationIDList,
 | 
			
		||||
                                proxy_id: monitorListData[i].proxy_id || null,
 | 
			
		||||
                            };
 | 
			
		||||
 | 
			
		||||
@@ -1527,7 +1549,6 @@ let needSetup = false;
 | 
			
		||||
        maintenanceSocketHandler(socket);
 | 
			
		||||
        apiKeySocketHandler(socket);
 | 
			
		||||
        generalSocketHandler(socket, server);
 | 
			
		||||
        pluginsHandler(socket, server);
 | 
			
		||||
 | 
			
		||||
        log.debug("server", "added all socket handlers");
 | 
			
		||||
 | 
			
		||||
@@ -1630,6 +1651,7 @@ async function afterLogin(socket, user) {
 | 
			
		||||
    socket.join(user.id);
 | 
			
		||||
 | 
			
		||||
    let monitorList = await server.sendMonitorList(socket);
 | 
			
		||||
    sendInfo(socket);
 | 
			
		||||
    server.sendMaintenanceList(socket);
 | 
			
		||||
    sendNotificationList(socket);
 | 
			
		||||
    sendProxyList(socket);
 | 
			
		||||
@@ -1697,7 +1719,7 @@ async function initDatabase(testMode = false) {
 | 
			
		||||
        needSetup = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    jwtSecret = jwtSecretBean.value;
 | 
			
		||||
    server.jwtSecret = jwtSecretBean.value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
 | 
			
		||||
const { sendInfo } = require("../client");
 | 
			
		||||
const { checkLogin } = require("../util-server");
 | 
			
		||||
const GameResolver = require("gamedig/lib/GameResolver");
 | 
			
		||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
 | 
			
		||||
 | 
			
		||||
let gameResolver = new GameResolver();
 | 
			
		||||
let gameList = null;
 | 
			
		||||
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("testChrome", (executable, callback) => {
 | 
			
		||||
        // Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
 | 
			
		||||
        testChrome(executable).then((version) => {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                msg: "Found Chromium/Chrome. Version: " + version,
 | 
			
		||||
            });
 | 
			
		||||
        }).catch((e) => {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: e.message,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
const { checkLogin } = require("../util-server");
 | 
			
		||||
const { PluginsManager } = require("../plugins-manager");
 | 
			
		||||
const { log } = require("../../src/util.js");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handlers for plugins
 | 
			
		||||
 * @param {Socket} socket Socket.io instance
 | 
			
		||||
 * @param {UptimeKumaServer} server
 | 
			
		||||
 */
 | 
			
		||||
module.exports.pluginsHandler = (socket, server) => {
 | 
			
		||||
 | 
			
		||||
    const pluginManager = server.getPluginManager();
 | 
			
		||||
 | 
			
		||||
    // Get Plugin List
 | 
			
		||||
    socket.on("getPluginList", async (callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
 | 
			
		||||
            log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
 | 
			
		||||
 | 
			
		||||
            if (PluginsManager.disable) {
 | 
			
		||||
                throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let pluginList = await pluginManager.fetchPluginList();
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                pluginList,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            log.warn("plugin", "Error: " + error.message);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("installPlugin", async (repoURL, name, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
            pluginManager.downloadPlugin(repoURL, name);
 | 
			
		||||
            await pluginManager.loadPlugin(name);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("uninstallPlugin", async (name, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
            await pluginManager.removePlugin(name);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@@ -10,8 +10,7 @@ const util = require("util");
 | 
			
		||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
const { PluginsManager } = require("./plugins-manager");
 | 
			
		||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
 | 
			
		||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
 | 
			
		||||
@@ -47,12 +46,6 @@ class UptimeKumaServer {
 | 
			
		||||
     */
 | 
			
		||||
    indexHTML = "";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugins Manager
 | 
			
		||||
     * @type {PluginsManager}
 | 
			
		||||
     */
 | 
			
		||||
    pluginsManager = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @type {{}}
 | 
			
		||||
@@ -61,6 +54,12 @@ class UptimeKumaServer {
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Use for decode the auth object
 | 
			
		||||
     * @type {null}
 | 
			
		||||
     */
 | 
			
		||||
    jwtSecret = null;
 | 
			
		||||
 | 
			
		||||
    static getInstance(args) {
 | 
			
		||||
        if (UptimeKumaServer.instance == null) {
 | 
			
		||||
            UptimeKumaServer.instance = new UptimeKumaServer(args);
 | 
			
		||||
@@ -98,11 +97,17 @@ class UptimeKumaServer {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set Monitor Types
 | 
			
		||||
        UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
 | 
			
		||||
 | 
			
		||||
        this.io = new Server(this.httpServer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Initialise app after the database has been set up */
 | 
			
		||||
    async initAfterDatabaseReady() {
 | 
			
		||||
        // Static
 | 
			
		||||
        this.app.use("/screenshots", express.static(Database.screenshotDir));
 | 
			
		||||
 | 
			
		||||
        await CacheableDnsHttpAgent.update();
 | 
			
		||||
 | 
			
		||||
        process.env.TZ = await this.getTimezone();
 | 
			
		||||
@@ -244,9 +249,9 @@ class UptimeKumaServer {
 | 
			
		||||
 | 
			
		||||
            return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
 | 
			
		||||
                || socket.client.conn.request.headers["x-real-ip"]
 | 
			
		||||
                || clientIP.replace(/^.*:/, "");
 | 
			
		||||
                || clientIP.replace(/^::ffff:/, "");
 | 
			
		||||
        } else {
 | 
			
		||||
            return clientIP.replace(/^.*:/, "");
 | 
			
		||||
            return clientIP.replace(/^::ffff:/, "");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -257,13 +262,43 @@ class UptimeKumaServer {
 | 
			
		||||
     * @returns {Promise<string>}
 | 
			
		||||
     */
 | 
			
		||||
    async getTimezone() {
 | 
			
		||||
        // From process.env.TZ
 | 
			
		||||
        try {
 | 
			
		||||
            if (process.env.TZ) {
 | 
			
		||||
                this.checkTimezone(process.env.TZ);
 | 
			
		||||
                return process.env.TZ;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.warn("timezone", e.message + " in process.env.TZ");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let timezone = await Settings.get("serverTimezone");
 | 
			
		||||
        if (timezone) {
 | 
			
		||||
            return timezone;
 | 
			
		||||
        } else if (process.env.TZ) {
 | 
			
		||||
            return process.env.TZ;
 | 
			
		||||
        } else {
 | 
			
		||||
            return dayjs.tz.guess();
 | 
			
		||||
 | 
			
		||||
        // From Settings
 | 
			
		||||
        try {
 | 
			
		||||
            log.debug("timezone", "Using timezone from settings: " + timezone);
 | 
			
		||||
            if (timezone) {
 | 
			
		||||
                this.checkTimezone(timezone);
 | 
			
		||||
                return timezone;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.warn("timezone", e.message + " in settings");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Guess
 | 
			
		||||
        try {
 | 
			
		||||
            let guess = dayjs.tz.guess();
 | 
			
		||||
            log.debug("timezone", "Guessing timezone: " + guess);
 | 
			
		||||
            if (guess) {
 | 
			
		||||
                this.checkTimezone(guess);
 | 
			
		||||
                return guess;
 | 
			
		||||
            } else {
 | 
			
		||||
                return "UTC";
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // Guess failed, fall back to UTC
 | 
			
		||||
            log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
 | 
			
		||||
            return "UTC";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -275,11 +310,24 @@ class UptimeKumaServer {
 | 
			
		||||
        return dayjs().format("Z");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Throw an error if the timezone is invalid
 | 
			
		||||
     * @param timezone
 | 
			
		||||
     */
 | 
			
		||||
    checkTimezone(timezone) {
 | 
			
		||||
        try {
 | 
			
		||||
            dayjs.utc("2013-11-18 11:55").tz(timezone).format();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw new Error("Invalid timezone:" + timezone);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the current server timezone and environment variables
 | 
			
		||||
     * @param {string} timezone
 | 
			
		||||
     */
 | 
			
		||||
    async setTimezone(timezone) {
 | 
			
		||||
        this.checkTimezone(timezone);
 | 
			
		||||
        await Settings.set("serverTimezone", timezone, "general");
 | 
			
		||||
        process.env.TZ = timezone;
 | 
			
		||||
        dayjs.tz.setDefault(timezone);
 | 
			
		||||
@@ -289,51 +337,11 @@ class UptimeKumaServer {
 | 
			
		||||
    async stop() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadPlugins() {
 | 
			
		||||
        this.pluginsManager = new PluginsManager(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @returns {PluginsManager}
 | 
			
		||||
     */
 | 
			
		||||
    getPluginManager() {
 | 
			
		||||
        return this.pluginsManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {MonitorType} monitorType
 | 
			
		||||
     */
 | 
			
		||||
    addMonitorType(monitorType) {
 | 
			
		||||
        if (monitorType instanceof MonitorType && monitorType.name) {
 | 
			
		||||
            if (monitorType.name in UptimeKumaServer.monitorTypeList) {
 | 
			
		||||
                log.error("", "Conflict Monitor Type name");
 | 
			
		||||
            }
 | 
			
		||||
            UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
 | 
			
		||||
        } else {
 | 
			
		||||
            log.error("", "Invalid Monitor Type: " + monitorType.name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {MonitorType} monitorType
 | 
			
		||||
     */
 | 
			
		||||
    removeMonitorType(monitorType) {
 | 
			
		||||
        if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
 | 
			
		||||
            delete UptimeKumaServer.monitorTypeList[monitorType.name];
 | 
			
		||||
        } else {
 | 
			
		||||
            log.error("", "Remove MonitorType failed: " + monitorType.name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    UptimeKumaServer
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Must be at the end
 | 
			
		||||
const { MonitorType } = require("./monitor-types/monitor-type");
 | 
			
		||||
// Must be at the end to avoid circular dependencies
 | 
			
		||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
 | 
			
		||||
 
 | 
			
		||||
@@ -378,6 +378,7 @@ exports.mongodbPing = async function (connectionString) {
 | 
			
		||||
 * @param {string} callingStationId ID of calling station
 | 
			
		||||
 * @param {string} secret Secret to use
 | 
			
		||||
 * @param {number} [port=1812] Port to contact radius server on
 | 
			
		||||
 * @param {number} [timeout=2500] Timeout for connection to use
 | 
			
		||||
 * @returns {Promise<any>}
 | 
			
		||||
 */
 | 
			
		||||
exports.radius = function (
 | 
			
		||||
@@ -388,10 +389,12 @@ exports.radius = function (
 | 
			
		||||
    callingStationId,
 | 
			
		||||
    secret,
 | 
			
		||||
    port = 1812,
 | 
			
		||||
    timeout = 2500,
 | 
			
		||||
) {
 | 
			
		||||
    const client = new radiusClient({
 | 
			
		||||
        host: hostname,
 | 
			
		||||
        hostPort: port,
 | 
			
		||||
        timeout: timeout,
 | 
			
		||||
        dictionaries: [ file ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -413,12 +416,18 @@ exports.radius = function (
 | 
			
		||||
exports.redisPingAsync = function (dsn) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        const client = redis.createClient({
 | 
			
		||||
            url: dsn,
 | 
			
		||||
            url: dsn
 | 
			
		||||
        });
 | 
			
		||||
        client.on("error", (err) => {
 | 
			
		||||
            if (client.isOpen) {
 | 
			
		||||
                client.disconnect();
 | 
			
		||||
            }
 | 
			
		||||
            reject(err);
 | 
			
		||||
        });
 | 
			
		||||
        client.connect().then(() => {
 | 
			
		||||
            if (!client.isOpen) {
 | 
			
		||||
                client.emit("error", new Error("connection isn't open"));
 | 
			
		||||
            }
 | 
			
		||||
            client.ping().then((res, err) => {
 | 
			
		||||
                if (client.isOpen) {
 | 
			
		||||
                    client.disconnect();
 | 
			
		||||
@@ -428,7 +437,7 @@ exports.redisPingAsync = function (dsn) {
 | 
			
		||||
                } else {
 | 
			
		||||
                    resolve(res);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            }).catch(error => reject(error));
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,7 @@
 | 
			
		||||
    .multiselect__content-wrapper {
 | 
			
		||||
        background-color: $dark-bg2;
 | 
			
		||||
        border-color: $dark-border-color;
 | 
			
		||||
        z-index: 150;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .multiselect--above .multiselect__content-wrapper {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,78 +22,78 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
 | 
			
		||||
                        <label for="duration" class="form-label">{{ $t("Badge Duration") }}</label>
 | 
			
		||||
                        <input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required>
 | 
			
		||||
                        <label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
 | 
			
		||||
                        <input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
 | 
			
		||||
                        <label for="label" class="form-label">{{ $t("Badge Label") }}</label>
 | 
			
		||||
                        <input id="label" v-model="badge.label" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="label" v-model="badge.label" type="text" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
 | 
			
		||||
                        <label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
 | 
			
		||||
                        <input id="prefix" v-model="badge.prefix" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="prefix" v-model="badge.prefix" type="text" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
 | 
			
		||||
                        <label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
 | 
			
		||||
                        <input id="suffix" v-model="badge.suffix" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="suffix" v-model="badge.suffix" type="text" placeholder="%" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
 | 
			
		||||
                        <label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
 | 
			
		||||
                        <input id="labelColor" v-model="badge.labelColor" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="labelColor" v-model="badge.labelColor" type="text" placeholder="#555" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
 | 
			
		||||
                        <label for="color" class="form-label">{{ $t("Badge Color") }}</label>
 | 
			
		||||
                        <input id="color" v-model="badge.color" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="color" v-model="badge.color" type="text" :placeholder="badgeConstants.defaultUpColor" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
 | 
			
		||||
                        <label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
 | 
			
		||||
                        <input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
 | 
			
		||||
                        <label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
 | 
			
		||||
                        <input id="labelSuffix" v-model="badge.labelSuffix" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="labelSuffix" v-model="badge.labelSuffix" type="text" placeholder="h" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
 | 
			
		||||
                        <label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
 | 
			
		||||
                        <input id="upColor" v-model="badge.upColor" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="upColor" v-model="badge.upColor" type="text" class="form-control" :placeholder="badgeConstants.defaultUpColor">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
 | 
			
		||||
                        <label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
 | 
			
		||||
                        <input id="downColor" v-model="badge.downColor" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="downColor" v-model="badge.downColor" type="text" class="form-control" :placeholder="badgeConstants.defaultDownColor">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
 | 
			
		||||
                        <label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
 | 
			
		||||
                        <input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" :placeholder="badgeConstants.defaultPendingColor">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
 | 
			
		||||
                        <label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
 | 
			
		||||
                        <input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
 | 
			
		||||
                        <label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
 | 
			
		||||
                        <input id="warnColor" v-model="badge.warnColor" type="number" min="0" class="form-control" required>
 | 
			
		||||
                        <input id="warnColor" v-model="badge.warnColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
 | 
			
		||||
                        <label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
 | 
			
		||||
                        <input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" required>
 | 
			
		||||
                        <input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireWarnDays">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
 | 
			
		||||
                        <label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
 | 
			
		||||
                        <input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" required>
 | 
			
		||||
                        <input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
@@ -109,12 +109,16 @@
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
 | 
			
		||||
                        <input id="value" v-model="badge.value" type="text" class="form-control" required>
 | 
			
		||||
                        <input id="value" v-model="badge.value" type="text" class="form-control">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3 pt-3 d-flex justify-content-center">
 | 
			
		||||
                        <img :src="badgeURL" :alt="$t('Badge Preview')">
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="my-3">
 | 
			
		||||
                        <label for="push-url" class="form-label">{{ $t("Badge URL") }}</label>
 | 
			
		||||
                        <CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" />
 | 
			
		||||
                        <label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
 | 
			
		||||
                        <CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@@ -131,6 +135,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { Modal } from "bootstrap";
 | 
			
		||||
import CopyableInput from "./CopyableInput.vue";
 | 
			
		||||
import { default as serverConfig } from "../../server/config.js";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
@@ -224,7 +229,8 @@ export default {
 | 
			
		||||
                    "color",
 | 
			
		||||
                    "labelColor",
 | 
			
		||||
                ],
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            badgeConstants: serverConfig.badgeConstants,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,7 @@ export default {
 | 
			
		||||
            // We must check if there are any elements in monitorList to
 | 
			
		||||
            // prevent undefined errors if it hasn't been loaded yet
 | 
			
		||||
            if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
 | 
			
		||||
                return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
 | 
			
		||||
                return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
 | 
			
		||||
            }
 | 
			
		||||
            return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -164,6 +164,7 @@ export default {
 | 
			
		||||
                "SMSManager": "SmsManager (smsmanager.cz)",
 | 
			
		||||
                "WeCom": "WeCom (企业微信群机器人)",
 | 
			
		||||
                "ServerChan": "ServerChan (Server酱)",
 | 
			
		||||
                "smsc": "SMSC",
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Sort by notification name
 | 
			
		||||
 
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
 | 
			
		||||
        <div class="info">
 | 
			
		||||
            <h5>{{ plugin.fullName }}</h5>
 | 
			
		||||
            <p class="description">
 | 
			
		||||
                {{ plugin.description }}
 | 
			
		||||
            </p>
 | 
			
		||||
            <span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
 | 
			
		||||
            <button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
 | 
			
		||||
            <button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
 | 
			
		||||
            <button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
 | 
			
		||||
            {{ $t("confirmUninstallPlugin") }}
 | 
			
		||||
        </Confirm>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Confirm from "./Confirm.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        Confirm,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        plugin: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            status: "",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        /**
 | 
			
		||||
         * Show confirmation for deleting a tag
 | 
			
		||||
         */
 | 
			
		||||
        deleteConfirm() {
 | 
			
		||||
            this.$refs.confirmDelete.show();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        install() {
 | 
			
		||||
            this.status = "installing";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.status = "";
 | 
			
		||||
                    // eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
                    this.plugin.installed = true;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.$root.toastRes(res);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        uninstall() {
 | 
			
		||||
            this.status = "uninstalling";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.status = "";
 | 
			
		||||
                    // eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
                    this.plugin.installed = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.$root.toastRes(res);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "../assets/vars.scss";
 | 
			
		||||
 | 
			
		||||
.plugin-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    .info {
 | 
			
		||||
        margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .description {
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .version {
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -150,7 +150,7 @@ export default {
 | 
			
		||||
            // We must check if there are any elements in monitorList to
 | 
			
		||||
            // prevent undefined errors if it hasn't been loaded yet
 | 
			
		||||
            if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
 | 
			
		||||
                return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
 | 
			
		||||
                return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
 | 
			
		||||
            }
 | 
			
		||||
            return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								src/components/notifications/SMSC.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/components/notifications/SMSC.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="smsc-login" class="form-label">{{ $t("API Username") }}</label>
 | 
			
		||||
        <i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
 | 
			
		||||
            <a href="https://smsc.kz/" target="_blank">{{ $t("here") }}</a>
 | 
			
		||||
        </i18n-t>
 | 
			
		||||
        <input id="smsc-login" v-model="$parent.notification.smscLogin" type="text" class="form-control" required>
 | 
			
		||||
        <label for="smsc-key" class="form-label">{{ $t("API Key") }}</label>
 | 
			
		||||
        <HiddenInput id="smsc-key" v-model="$parent.notification.smscPassword" :required="true" autocomplete="new-password"></HiddenInput>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            {{ $t("checkPrice", ['СМСЦ']) }}
 | 
			
		||||
            <a href="https://smsc.kz/tariffs/" target="_blank">https://smsc.kz/tariffs/</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="smsc-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
 | 
			
		||||
        <input id="smsc-to-number" v-model="$parent.notification.smscToNumber" type="text" minlength="11" class="form-control" required>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="smsc-sender-name" class="form-label">{{ $t("From Name/Number") }}</label>
 | 
			
		||||
        <input id="smsc-sender-name" v-model="$parent.notification.smscSenderName" type="text" minlength="1" maxlength="15" class="form-control">
 | 
			
		||||
        <div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="smsc-platform" class="form-label">{{ $t("smscTranslit") }}</label><span style="color: red;"><sup>*</sup></span>
 | 
			
		||||
        <select id="smsc-platform" v-model="$parent.notification.smscTranslit" class="form-select">
 | 
			
		||||
            <option value="0">{{ $t("Default") }}</option>
 | 
			
		||||
            <option value="1">Translit</option>
 | 
			
		||||
            <option value="2">MpaHc/Ium</option>
 | 
			
		||||
        </select>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import HiddenInput from "../HiddenInput.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        HiddenInput,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -24,5 +24,13 @@
 | 
			
		||||
                <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
 | 
			
		||||
            </i18n-t>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="form-check form-switch">
 | 
			
		||||
            <input id="slack-channel-notify" v-model="$parent.notification.slackchannelnotify" type="checkbox" class="form-check-input">
 | 
			
		||||
            <label for="slack-channel-notify" class="form-label">{{ $t("Notify Channel") }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            {{ $t("aboutNotifyChannel") }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,18 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
 | 
			
		||||
        <label for="twilio-apikey-token" class="form-label">{{ $t("Api Key (optional)") }}</label>
 | 
			
		||||
        <input id="twilio-apikey-token" v-model="$parent.notification.twilioApiKey" type="text" class="form-control">
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            <p>
 | 
			
		||||
                The API key is optional but recommended. You can provide either Account SID and AuthToken
 | 
			
		||||
                from the may TwilioConsole page or Account SID and the pair of Api Key and Api Key secret
 | 
			
		||||
            </p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="twilio-auth-token" class="form-label">{{ $t("Auth Token / Api Key Secret") }}</label>
 | 
			
		||||
        <input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,61 +12,97 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <label for="webhook-content-type" class="form-label">{{
 | 
			
		||||
            $t("Content Type")
 | 
			
		||||
        <label for="webhook-request-body" class="form-label">{{
 | 
			
		||||
            $t("Request Body")
 | 
			
		||||
        }}</label>
 | 
			
		||||
        <select
 | 
			
		||||
            id="webhook-content-type"
 | 
			
		||||
            id="webhook-request-body"
 | 
			
		||||
            v-model="$parent.notification.webhookContentType"
 | 
			
		||||
            class="form-select"
 | 
			
		||||
            required
 | 
			
		||||
        >
 | 
			
		||||
            <option value="json">application/json</option>
 | 
			
		||||
            <option value="form-data">multipart/form-data</option>
 | 
			
		||||
            <option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
 | 
			
		||||
            <option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
 | 
			
		||||
            <option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
 | 
			
		||||
        </select>
 | 
			
		||||
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
 | 
			
		||||
            <i18n-t tag="p" keypath="webhookFormDataDesc">
 | 
			
		||||
                <template #multipart>"multipart/form-data"</template>
 | 
			
		||||
                <template #decodeFunction>
 | 
			
		||||
                    <strong>json_decode($_POST['data'])</strong>
 | 
			
		||||
                </template>
 | 
			
		||||
            </i18n-t>
 | 
			
		||||
            <div v-if="$parent.notification.webhookContentType == 'json'">
 | 
			
		||||
                <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div v-if="$parent.notification.webhookContentType == 'form-data'">
 | 
			
		||||
                <i18n-t tag="p" keypath="webhookFormDataDesc">
 | 
			
		||||
                    <template #multipart>multipart/form-data"</template>
 | 
			
		||||
                    <template #decodeFunction>
 | 
			
		||||
                        <strong>json_decode($_POST['data'])</strong>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </i18n-t>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div v-if="$parent.notification.webhookContentType == 'custom'">
 | 
			
		||||
                <i18n-t tag="p" keypath="webhookCustomBodyDesc">
 | 
			
		||||
                    <template #msg>
 | 
			
		||||
                        <code>msg</code>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <template #heartbeat>
 | 
			
		||||
                        <code>heartbeatJSON</code>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <template #monitor>
 | 
			
		||||
                        <code>monitorJSON</code>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </i18n-t>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <textarea
 | 
			
		||||
            v-if="$parent.notification.webhookContentType == 'custom'"
 | 
			
		||||
            id="customBody"
 | 
			
		||||
            v-model="$parent.notification.webhookCustomBody"
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            :placeholder="customBodyPlaceholder"
 | 
			
		||||
        ></textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3">
 | 
			
		||||
        <i18n-t
 | 
			
		||||
            tag="label"
 | 
			
		||||
            class="form-label"
 | 
			
		||||
            for="additionalHeaders"
 | 
			
		||||
            keypath="webhookAdditionalHeadersTitle"
 | 
			
		||||
        >
 | 
			
		||||
        </i18n-t>
 | 
			
		||||
        <div class="form-check form-switch">
 | 
			
		||||
            <input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
 | 
			
		||||
            <label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
 | 
			
		||||
        </div>
 | 
			
		||||
        <textarea
 | 
			
		||||
            v-if="showAdditionalHeadersField"
 | 
			
		||||
            id="additionalHeaders"
 | 
			
		||||
            v-model="$parent.notification.webhookAdditionalHeaders"
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            :placeholder="headersPlaceholder"
 | 
			
		||||
        ></textarea>
 | 
			
		||||
        <div class="form-text">
 | 
			
		||||
            <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        headersPlaceholder() {
 | 
			
		||||
            return this.$t("Example:", [
 | 
			
		||||
                `
 | 
			
		||||
{
 | 
			
		||||
    "HeaderName": "HeaderValue"
 | 
			
		||||
    "Authorization": "Authorization Token"
 | 
			
		||||
}`,
 | 
			
		||||
            ]);
 | 
			
		||||
        },
 | 
			
		||||
        customBodyPlaceholder() {
 | 
			
		||||
            return `Example:
 | 
			
		||||
{
 | 
			
		||||
    "Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
 | 
			
		||||
    "Body": "{{ msg }}"
 | 
			
		||||
}`;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
 | 
			
		||||
import Apprise from "./Apprise.vue";
 | 
			
		||||
import Bark from "./Bark.vue";
 | 
			
		||||
import ClickSendSMS from "./ClickSendSMS.vue";
 | 
			
		||||
import SMSC from "./SMSC.vue";
 | 
			
		||||
import DingDing from "./DingDing.vue";
 | 
			
		||||
import Discord from "./Discord.vue";
 | 
			
		||||
import Feishu from "./Feishu.vue";
 | 
			
		||||
@@ -61,6 +62,7 @@ const NotificationFormList = {
 | 
			
		||||
    "apprise": Apprise,
 | 
			
		||||
    "Bark": Bark,
 | 
			
		||||
    "clicksendsms": ClickSendSMS,
 | 
			
		||||
    "smsc": SMSC,
 | 
			
		||||
    "DingDing": DingDing,
 | 
			
		||||
    "discord": Discord,
 | 
			
		||||
    "Feishu": Feishu,
 | 
			
		||||
 
 | 
			
		||||
@@ -190,6 +190,30 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Chrome Executable -->
 | 
			
		||||
            <div class="mb-4">
 | 
			
		||||
                <label class="form-label" for="primaryBaseURL">
 | 
			
		||||
                    {{ $t("chromeExecutable") }}
 | 
			
		||||
                </label>
 | 
			
		||||
 | 
			
		||||
                <div class="input-group mb-3">
 | 
			
		||||
                    <input
 | 
			
		||||
                        id="primaryBaseURL"
 | 
			
		||||
                        v-model="settings.chromeExecutable"
 | 
			
		||||
                        class="form-control"
 | 
			
		||||
                        name="primaryBaseURL"
 | 
			
		||||
                        :placeholder="$t('chromeExecutableAutoDetect')"
 | 
			
		||||
                    />
 | 
			
		||||
                    <button class="btn btn-outline-primary" type="button" @click="testChrome">
 | 
			
		||||
                        {{ $t("Test") }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-text">
 | 
			
		||||
                    {{ $t("chromeExecutableDescription") }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Save Button -->
 | 
			
		||||
            <div>
 | 
			
		||||
                <button class="btn btn-primary" type="submit">
 | 
			
		||||
@@ -241,6 +265,12 @@ export default {
 | 
			
		||||
        autoGetPrimaryBaseURL() {
 | 
			
		||||
            this.settings.primaryBaseURL = location.protocol + "//" + location.host;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        testChrome() {
 | 
			
		||||
            this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
 | 
			
		||||
                this.$root.toastRes(res);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="mt-3">{{ remotePluginListMsg }}</div>
 | 
			
		||||
        <PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import PluginItem from "../PluginItem.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        PluginItem
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            remotePluginList: [],
 | 
			
		||||
            remotePluginListMsg: "",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        pluginList() {
 | 
			
		||||
            return this.$parent.$parent.$parent.pluginList;
 | 
			
		||||
        },
 | 
			
		||||
        settings() {
 | 
			
		||||
            return this.$parent.$parent.$parent.settings;
 | 
			
		||||
        },
 | 
			
		||||
        saveSettings() {
 | 
			
		||||
            return this.$parent.$parent.$parent.saveSettings;
 | 
			
		||||
        },
 | 
			
		||||
        settingsLoaded() {
 | 
			
		||||
            return this.$parent.$parent.$parent.settingsLoaded;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async mounted() {
 | 
			
		||||
        this.loadList();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        loadList() {
 | 
			
		||||
            this.remotePluginListMsg = this.$t("Loading") + "...";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("getPluginList", (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.remotePluginList = res.pluginList;
 | 
			
		||||
                    this.remotePluginListMsg = "";
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -776,5 +776,13 @@
 | 
			
		||||
    "Badge Suffix": "Суфикс на баджа",
 | 
			
		||||
    "Badge Label Prefix": "Префикс на етикета на значката",
 | 
			
		||||
    "Badge Pending Color": "Цвят на баджа за изчакващ",
 | 
			
		||||
    "Badge Down Days": "Колко дни баджът да не се показва"
 | 
			
		||||
    "Badge Down Days": "Колко дни баджът да не се показва",
 | 
			
		||||
    "Group": "Група",
 | 
			
		||||
    "Monitor Group": "Монитор група",
 | 
			
		||||
    "Cannot connect to the socket server": "Не може да се свърже със сокет сървъра",
 | 
			
		||||
    "Reconnecting...": "Повторно свързване...",
 | 
			
		||||
    "Edit Maintenance": "Редактиране на поддръжка",
 | 
			
		||||
    "Home": "Главна страница",
 | 
			
		||||
    "noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
 | 
			
		||||
    "Close": "Затвори"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/lang/ca.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lang/ca.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
{
 | 
			
		||||
    "Settings": "Paràmetres",
 | 
			
		||||
    "Dashboard": "Tauler",
 | 
			
		||||
    "Help": "Ajuda",
 | 
			
		||||
    "New Update": "Nova actualització",
 | 
			
		||||
    "Language": "Idioma",
 | 
			
		||||
    "Appearance": "Aparença",
 | 
			
		||||
    "Theme": "Tema",
 | 
			
		||||
    "General": "General",
 | 
			
		||||
    "Game": "Joc",
 | 
			
		||||
    "Version": "Versió",
 | 
			
		||||
    "Check Update On GitHub": "Comprovar actualitzacions a GitHub",
 | 
			
		||||
    "List": "Llista",
 | 
			
		||||
    "Home": "Inici",
 | 
			
		||||
    "Add": "Afegir",
 | 
			
		||||
    "Add New Monitor": "Afegir nou monitor",
 | 
			
		||||
    "Quick Stats": "Estadístiques ràpides",
 | 
			
		||||
    "Up": "Funcional",
 | 
			
		||||
    "Down": "Caigut",
 | 
			
		||||
    "Pending": "Pendent",
 | 
			
		||||
    "Maintenance": "Manteniment",
 | 
			
		||||
    "Unknown": "Desconegut",
 | 
			
		||||
    "Cannot connect to the socket server": "No es pot connectar al servidor socket",
 | 
			
		||||
    "Reconnecting...": "S'està tornant a connectar...",
 | 
			
		||||
    "languageName": "Català",
 | 
			
		||||
    "Primary Base URL": "URL Base Primària",
 | 
			
		||||
    "statusMaintenance": "Manteniment"
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +1,46 @@
 | 
			
		||||
{}
 | 
			
		||||
{
 | 
			
		||||
    "languageName": "کوردی",
 | 
			
		||||
    "Settings": "ڕێکخستنەکان",
 | 
			
		||||
    "Help": "یارمەتی",
 | 
			
		||||
    "New Update": "وەشانی نوێ",
 | 
			
		||||
    "Language": "زمان",
 | 
			
		||||
    "Appearance": "ڕووکار",
 | 
			
		||||
    "Theme": "شێوەی ڕووکار",
 | 
			
		||||
    "General": "گشتی",
 | 
			
		||||
    "Game": "یاری",
 | 
			
		||||
    "Version": "وەشان",
 | 
			
		||||
    "Check Update On GitHub": "سەیری وەشانی نوێ بکە لە Github",
 | 
			
		||||
    "List": "لیست",
 | 
			
		||||
    "Add": "زیادکردن",
 | 
			
		||||
    "Quick Stats": "ئاماری خێرا",
 | 
			
		||||
    "Up": "سەروو",
 | 
			
		||||
    "Down": "خواروو",
 | 
			
		||||
    "Pending": "هەڵپەسێردراو",
 | 
			
		||||
    "statusMaintenance": "چاکردنەوە",
 | 
			
		||||
    "Maintenance": "چاکردنەوە",
 | 
			
		||||
    "Unknown": "نەزانراو",
 | 
			
		||||
    "Passive Monitor Type": "جۆری مۆنیتەری پاسیڤ",
 | 
			
		||||
    "Specific Monitor Type": "جۆری مۆنیتەری تایبەت",
 | 
			
		||||
    "markdownSupported": "ڕستەسازی مارکداون پشتگیری دەکرێت",
 | 
			
		||||
    "pauseDashboardHome": "وچان",
 | 
			
		||||
    "Pause": "وچان",
 | 
			
		||||
    "Name": "ناو",
 | 
			
		||||
    "Status": "دۆخ",
 | 
			
		||||
    "Message": "پەیام",
 | 
			
		||||
    "No important events": "هیچ ڕووداوێکی گرنگ نییە",
 | 
			
		||||
    "Resume": "دەستپێکردنەوە",
 | 
			
		||||
    "Edit": "بژارکردن",
 | 
			
		||||
    "Delete": "سڕینەوە",
 | 
			
		||||
    "Uptime": "کاتی کارکردن",
 | 
			
		||||
    "Cert Exp.": "بەسەرچوونی بڕوانامەی SSL.",
 | 
			
		||||
    "day": "ڕۆژ | ڕۆژەکان",
 | 
			
		||||
    "-day": "-ڕۆژ",
 | 
			
		||||
    "hour": "کاتژمێر",
 | 
			
		||||
    "Dashboard": "داشبۆرد",
 | 
			
		||||
    "Primary Base URL": "بەستەری بنچینەیی سەرەکی",
 | 
			
		||||
    "Add New Monitor": "مۆنیتەرێکی نوێ زیاد بکە",
 | 
			
		||||
    "General Monitor Type": "جۆری مۆنیتەری گشتی",
 | 
			
		||||
    "DateTime": "رێکەوت",
 | 
			
		||||
    "Current": "هەنووکە",
 | 
			
		||||
    "Monitor": "مۆنیتەر | مۆنیتەرەکان"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -757,11 +757,11 @@
 | 
			
		||||
    "Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
 | 
			
		||||
    "Open Badge Generator": "Otevřít generátor odznaků",
 | 
			
		||||
    "Badge Type": "Typ odznaku",
 | 
			
		||||
    "Badge Duration": "Délka platnosti odznaku",
 | 
			
		||||
    "Badge Duration": "Platnost odznaku",
 | 
			
		||||
    "Badge Label": "Štítek odznaku",
 | 
			
		||||
    "Badge Prefix": "Prefix odznaku",
 | 
			
		||||
    "Monitor Setting": "{0}'s Nastavení dohledu",
 | 
			
		||||
    "Badge Generator": "{0}'s Generátor odznaků",
 | 
			
		||||
    "Badge Generator": "Generátor odznaků pro {0}",
 | 
			
		||||
    "Badge Label Color": "Barva štítku odznaku",
 | 
			
		||||
    "Badge Color": "Barva odznaku",
 | 
			
		||||
    "Badge Style": "Styl odznaku",
 | 
			
		||||
@@ -769,9 +769,20 @@
 | 
			
		||||
    "Badge URL": "URL odznaku",
 | 
			
		||||
    "Badge Suffix": "Přípona odznaku",
 | 
			
		||||
    "Badge Label Prefix": "Prefix štítku odznaku",
 | 
			
		||||
    "Badge Up Color": "Barva odzanaku při Běží",
 | 
			
		||||
    "Badge Up Color": "Barva odznaku při Běží",
 | 
			
		||||
    "Badge Down Color": "Barva odznaku při Nedostupné",
 | 
			
		||||
    "Badge Pending Color": "Barva odznaku při Pauze",
 | 
			
		||||
    "Badge Maintenance Color": "Barva odznaku při Údržbě",
 | 
			
		||||
    "Badge Warn Color": "Barva odznaku při Upozornění"
 | 
			
		||||
    "Badge Warn Color": "Barva odznaku při Upozornění",
 | 
			
		||||
    "Reconnecting...": "Obnovení spojení...",
 | 
			
		||||
    "Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
 | 
			
		||||
    "Edit Maintenance": "Upravit Údržbu",
 | 
			
		||||
    "Home": "Hlavní stránka",
 | 
			
		||||
    "Badge Down Days": "Odznak nedostupných dní",
 | 
			
		||||
    "Group": "Skupina",
 | 
			
		||||
    "Monitor Group": "Sledovaná skupina",
 | 
			
		||||
    "noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
 | 
			
		||||
    "Close": "Zavřít",
 | 
			
		||||
    "Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
 | 
			
		||||
    "Badge Warn Days": "Odznak dní s upozorněním"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -776,5 +776,10 @@
 | 
			
		||||
    "Badge Label Suffix": "Badge Label Suffix",
 | 
			
		||||
    "Badge value (For Testing only.)": "Badge Wert (nur für Tests)",
 | 
			
		||||
    "Show Clickable Link Description": "Wenn diese Option aktiviert ist, kann jeder, der Zugriff auf diese Statusseite hat, auf die Monitor URL zugreifen.",
 | 
			
		||||
    "Badge Down Color": "Badge Down Farbe"
 | 
			
		||||
    "Badge Down Color": "Badge Down Farbe",
 | 
			
		||||
    "Edit Maintenance": "Wartung bearbeiten",
 | 
			
		||||
    "Group": "Gruppe",
 | 
			
		||||
    "Monitor Group": "Monitor Gruppe",
 | 
			
		||||
    "noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
 | 
			
		||||
    "Close": "Schliessen"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -782,5 +782,7 @@
 | 
			
		||||
    "Badge Suffix": "Badge Suffix",
 | 
			
		||||
    "Badge Warn Days": "Badge Warnung Tage",
 | 
			
		||||
    "Group": "Gruppe",
 | 
			
		||||
    "Monitor Group": "Monitor Gruppe"
 | 
			
		||||
    "Monitor Group": "Monitor Gruppe",
 | 
			
		||||
    "noGroupMonitorMsg": "Nicht verfügbar. Erstelle zunächst einen Gruppenmonitor.",
 | 
			
		||||
    "Close": "Schließen"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,9 @@
 | 
			
		||||
    "Ping": "Ping",
 | 
			
		||||
    "Monitor Type": "Monitor Type",
 | 
			
		||||
    "Keyword": "Keyword",
 | 
			
		||||
    "Invert Keyword": "Invert Keyword",
 | 
			
		||||
    "Expected Value": "Expected Value",
 | 
			
		||||
    "Json Query": "Json Query",
 | 
			
		||||
    "Friendly Name": "Friendly Name",
 | 
			
		||||
    "URL": "URL",
 | 
			
		||||
    "Hostname": "Hostname",
 | 
			
		||||
@@ -195,8 +198,11 @@
 | 
			
		||||
    "Content Type": "Content Type",
 | 
			
		||||
    "webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
 | 
			
		||||
    "webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
 | 
			
		||||
    "webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
 | 
			
		||||
    "webhookAdditionalHeadersTitle": "Additional Headers",
 | 
			
		||||
    "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook.",
 | 
			
		||||
    "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
 | 
			
		||||
    "webhookBodyPresetOption": "Preset - {0}",
 | 
			
		||||
    "webhookBodyCustomOption": "Custom Body",
 | 
			
		||||
    "Webhook URL": "Webhook URL",
 | 
			
		||||
    "Application Token": "Application Token",
 | 
			
		||||
    "Server URL": "Server URL",
 | 
			
		||||
@@ -435,6 +441,9 @@
 | 
			
		||||
    "Enable DNS Cache": "Enable DNS Cache",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "Disable": "Disable",
 | 
			
		||||
    "chromeExecutable": "Chrome/Chromium Executable",
 | 
			
		||||
    "chromeExecutableAutoDetect": "Auto Detect",
 | 
			
		||||
    "chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
 | 
			
		||||
    "dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
 | 
			
		||||
    "Single Maintenance Window": "Single Maintenance Window",
 | 
			
		||||
    "Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
 | 
			
		||||
@@ -515,6 +524,8 @@
 | 
			
		||||
    "passwordNotMatchMsg": "The repeat password does not match.",
 | 
			
		||||
    "notificationDescription": "Notifications must be assigned to a monitor to function.",
 | 
			
		||||
    "keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
 | 
			
		||||
    "invertKeywordDescription": "Look for the keyword to be absent rather than present.",
 | 
			
		||||
    "jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out <a href='https://jsonata.org/'>jsonata.org</a> for the documentation about the query language. A playground can be found <a href='https://try.jsonata.org/'>here</a>.",
 | 
			
		||||
    "backupDescription": "You can backup all monitors and notifications into a JSON file.",
 | 
			
		||||
    "backupDescription2": "Note: history and event data is not included.",
 | 
			
		||||
    "backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
 | 
			
		||||
@@ -629,6 +640,8 @@
 | 
			
		||||
    "matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
 | 
			
		||||
    "matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
 | 
			
		||||
    "Channel Name": "Channel Name",
 | 
			
		||||
    "Notify Channel": "Notify Channel",
 | 
			
		||||
    "aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
 | 
			
		||||
    "Uptime Kuma URL": "Uptime Kuma URL",
 | 
			
		||||
    "Icon Emoji": "Icon Emoji",
 | 
			
		||||
    "signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
 | 
			
		||||
@@ -722,7 +735,8 @@
 | 
			
		||||
    "ntfyAuthenticationMethod": "Authentication Method",
 | 
			
		||||
    "ntfyUsernameAndPassword": "Username and Password",
 | 
			
		||||
    "twilioAccountSID": "Account SID",
 | 
			
		||||
    "twilioAuthToken": "Auth Token",
 | 
			
		||||
    "twilioApiKey": "Api Key (optional)",
 | 
			
		||||
    "twilioAuthToken": "Auth Token / Api Key Secret",
 | 
			
		||||
    "twilioFromNumber": "From Number",
 | 
			
		||||
    "twilioToNumber": "To Number",
 | 
			
		||||
    "Monitor Setting": "{0}'s Monitor Setting",
 | 
			
		||||
@@ -731,13 +745,14 @@
 | 
			
		||||
    "Open Badge Generator": "Open Badge Generator",
 | 
			
		||||
    "Badge Generator": "{0}'s Badge Generator",
 | 
			
		||||
    "Badge Type": "Badge Type",
 | 
			
		||||
    "Badge Duration": "Badge Duration",
 | 
			
		||||
    "Badge Duration (in hours)": "Badge Duration (in hours)",
 | 
			
		||||
    "Badge Label": "Badge Label",
 | 
			
		||||
    "Badge Prefix": "Badge Prefix",
 | 
			
		||||
    "Badge Suffix": "Badge Suffix",
 | 
			
		||||
    "Badge Prefix": "Badge Value Prefix",
 | 
			
		||||
    "Badge Suffix": "Badge Value Suffix",
 | 
			
		||||
    "Badge Label Color": "Badge Label Color",
 | 
			
		||||
    "Badge Color": "Badge Color",
 | 
			
		||||
    "Badge Label Prefix": "Badge Label Prefix",
 | 
			
		||||
    "Badge Preview": "Badge Preview",
 | 
			
		||||
    "Badge Label Suffix": "Badge Label Suffix",
 | 
			
		||||
    "Badge Up Color": "Badge Up Color",
 | 
			
		||||
    "Badge Down Color": "Badge Down Color",
 | 
			
		||||
@@ -751,5 +766,7 @@
 | 
			
		||||
    "Badge URL": "Badge URL",
 | 
			
		||||
    "Group": "Group",
 | 
			
		||||
    "Monitor Group": "Monitor Group",
 | 
			
		||||
    "noGroupMonitorMsg": "Not Available. Create a Group Monitor First."
 | 
			
		||||
    "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
 | 
			
		||||
    "Close": "Close",
 | 
			
		||||
    "Request Body": "Request Body"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -751,5 +751,7 @@
 | 
			
		||||
    "statusPageRefreshIn": "Reinicio en: {0}",
 | 
			
		||||
    "twilioAuthToken": "Token de Autentificación",
 | 
			
		||||
    "ntfyUsernameAndPassword": "Nombre de Usuario y Contraseña",
 | 
			
		||||
    "ntfyAuthenticationMethod": "Método de Autentificación"
 | 
			
		||||
    "ntfyAuthenticationMethod": "Método de Autentificación",
 | 
			
		||||
    "Cannot connect to the socket server": "No se puede conectar al servidor socket",
 | 
			
		||||
    "Reconnecting...": "Reconectando..."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -745,5 +745,13 @@
 | 
			
		||||
    "Show Clickable Link Description": "اگر انتخاب شود، همه کسانی که به این صفحه وضعیت دسترسی دارند میتوانند به صفحه مانیتور نیز دسترسی داشته باشند.",
 | 
			
		||||
    "Badge Up Color": "رنگ نشان زمانی که مانیتور بدون مشکل و بالا است",
 | 
			
		||||
    "Badge Pending Color": "رنگ نشان زمانی که مانیتور در حال انتظار است",
 | 
			
		||||
    "Badge Warn Days": "روزهایی که مانیتور در حالت هشدار است"
 | 
			
		||||
    "Badge Warn Days": "روزهایی که مانیتور در حالت هشدار است",
 | 
			
		||||
    "noGroupMonitorMsg": "موجود نیست. ابتدا یک گروه مانیتور جدید ایجاد کنید.",
 | 
			
		||||
    "Home": "خانه",
 | 
			
		||||
    "Edit Maintenance": "ویرایش تعمیر و نگهداری",
 | 
			
		||||
    "Cannot connect to the socket server": "عدم امکان ارتباط با سوکت سرور",
 | 
			
		||||
    "Reconnecting...": "ارتباط مجدد...",
 | 
			
		||||
    "Monitor Group": "گروه مانیتور",
 | 
			
		||||
    "Group": "گروه",
 | 
			
		||||
    "Close": "بستن"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
    "Add New Monitor": "Ajouter une nouvelle sonde",
 | 
			
		||||
    "Quick Stats": "Résumé",
 | 
			
		||||
    "Up": "En ligne",
 | 
			
		||||
    "Down": "Bas",
 | 
			
		||||
    "Down": "Hors ligne",
 | 
			
		||||
    "Pending": "En attente",
 | 
			
		||||
    "Unknown": "Inconnu",
 | 
			
		||||
    "Pause": "En pause",
 | 
			
		||||
@@ -88,8 +88,8 @@
 | 
			
		||||
    "Port": "Port",
 | 
			
		||||
    "Heartbeat Interval": "Intervalle de vérification",
 | 
			
		||||
    "Retries": "Essais",
 | 
			
		||||
    "Heartbeat Retry Interval": "Réessayer l'intervalle de vérification",
 | 
			
		||||
    "Resend Notification if Down X times consecutively": "Renvoyer la notification si en panne X fois consécutivement",
 | 
			
		||||
    "Heartbeat Retry Interval": "Intervalle de ré-essaie",
 | 
			
		||||
    "Resend Notification if Down X times consecutively": "Renvoyer la notification si hors ligne X fois consécutivement",
 | 
			
		||||
    "Advanced": "Avancé",
 | 
			
		||||
    "Upside Down Mode": "Mode inversé",
 | 
			
		||||
    "Max. Redirects": "Nombre maximum de redirections",
 | 
			
		||||
@@ -775,5 +775,14 @@
 | 
			
		||||
    "Monitor Setting": "Réglage de la sonde {0}",
 | 
			
		||||
    "Badge Generator": "Générateur de badges {0}",
 | 
			
		||||
    "Badge Label": "Étiquette de badge",
 | 
			
		||||
    "Badge URL": "URL du badge"
 | 
			
		||||
    "Badge URL": "URL du badge",
 | 
			
		||||
    "Cannot connect to the socket server": "Impossible de se connecter au serveur de socket",
 | 
			
		||||
    "Reconnecting...": "Reconnexion...",
 | 
			
		||||
    "Edit Maintenance": "Modifier la maintenance",
 | 
			
		||||
    "Monitor Group": "Groupe de sonde | Groupe de sondes",
 | 
			
		||||
    "Badge Down Days": "Badge hors ligne",
 | 
			
		||||
    "Group": "Groupe",
 | 
			
		||||
    "Home": "Accueil",
 | 
			
		||||
    "noGroupMonitorMsg": "Pas disponible. Créez d'abord une sonde de groupe.",
 | 
			
		||||
    "Close": "Fermer"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								src/lang/gl.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/lang/gl.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
    "Settings": "Axustes",
 | 
			
		||||
    "Dashboard": "Panel",
 | 
			
		||||
    "Help": "Axuda",
 | 
			
		||||
    "General": "Xeral",
 | 
			
		||||
    "List": "Lista",
 | 
			
		||||
    "Home": "Casa",
 | 
			
		||||
    "Add": "Engadir",
 | 
			
		||||
    "Up": "Arriba",
 | 
			
		||||
    "Pending": "Pendente",
 | 
			
		||||
    "statusMaintenance": "Mantemento",
 | 
			
		||||
    "Maintenance": "Mantemento",
 | 
			
		||||
    "Unknown": "Descoñecido",
 | 
			
		||||
    "Reconnecting...": "Reconectando...",
 | 
			
		||||
    "pauseDashboardHome": "Pausa",
 | 
			
		||||
    "Pause": "Pausa",
 | 
			
		||||
    "Name": "Nome",
 | 
			
		||||
    "Status": "Estado",
 | 
			
		||||
    "DateTime": "DataHora",
 | 
			
		||||
    "Message": "Mensaxe",
 | 
			
		||||
    "languageName": "Galego",
 | 
			
		||||
    "Down": "Abaixo"
 | 
			
		||||
}
 | 
			
		||||
@@ -724,5 +724,22 @@
 | 
			
		||||
    "Edit Tag": "עריכת תגית",
 | 
			
		||||
    "Learn More": "לקריאה נוספת",
 | 
			
		||||
    "telegramSendSilently": "שליחה שקטה",
 | 
			
		||||
    "telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל."
 | 
			
		||||
    "telegramSendSilentlyDescription": "שליחת הודעות שקטה. משתמשים יקבלו ההתראה ללא צליל.",
 | 
			
		||||
    "Add New Tag": "הוסף תג חדש",
 | 
			
		||||
    "Home": "ראשי",
 | 
			
		||||
    "sameAsServerTimezone": "אותו איזור זמן כמו השרת",
 | 
			
		||||
    "cronSchedule": "לו\"ז: ",
 | 
			
		||||
    "twilioToNumber": "למספר",
 | 
			
		||||
    "startDateTime": "תאריך\\זמן התחלה",
 | 
			
		||||
    "pagertreeSilent": "שקט",
 | 
			
		||||
    "Reconnecting...": "מתחבר מחדש...",
 | 
			
		||||
    "statusPageRefreshIn": "רענון תוך: {0}",
 | 
			
		||||
    "Edit Maintenance": "ערוך תחזוקה",
 | 
			
		||||
    "pagertreeUrgency": "דחיפות",
 | 
			
		||||
    "pagertreeLow": "נמוכה",
 | 
			
		||||
    "pagertreeMedium": "בינונית",
 | 
			
		||||
    "pagertreeHigh": "גבוהה",
 | 
			
		||||
    "pagertreeCritical": "קריטי",
 | 
			
		||||
    "pagertreeResolve": "הגדרה אוטומטית",
 | 
			
		||||
    "ntfyUsernameAndPassword": "שם משתמש וסיסמא"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								src/lang/hi.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/lang/hi.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
{
 | 
			
		||||
    "Dashboard": "डैशबोर्ड",
 | 
			
		||||
    "Help": "मदद",
 | 
			
		||||
    "New Update": "नया अपडेट",
 | 
			
		||||
    "Language": "भाषा",
 | 
			
		||||
    "Appearance": "अपीयरेंस",
 | 
			
		||||
    "Theme": "थीम",
 | 
			
		||||
    "Game": "गेम",
 | 
			
		||||
    "languageName": "हिंदी",
 | 
			
		||||
    "Settings": "सेटिंग्स",
 | 
			
		||||
    "General": "जनरल",
 | 
			
		||||
    "List": "सूची",
 | 
			
		||||
    "Add": "जोड़ें",
 | 
			
		||||
    "Add New Monitor": "नया मॉनिटर जोड़ें",
 | 
			
		||||
    "Pending": "लंबित",
 | 
			
		||||
    "statusMaintenance": "रखरखाव",
 | 
			
		||||
    "Maintenance": "रखरखाव",
 | 
			
		||||
    "Unknown": "अज्ञात",
 | 
			
		||||
    "Cannot connect to the socket server": "सॉकेट सर्वर से कनेक्ट नहीं हो सकता",
 | 
			
		||||
    "pauseDashboardHome": "विराम",
 | 
			
		||||
    "Resume": "फिर से शुरू करें",
 | 
			
		||||
    "Delete": "हटाएं",
 | 
			
		||||
    "Current": "मौजूदा",
 | 
			
		||||
    "Up": "चालू",
 | 
			
		||||
    "General Monitor Type": "सामान्य मॉनिटर प्रकार",
 | 
			
		||||
    "Specific Monitor Type": "विशिष्ट मॉनिटर प्रकार",
 | 
			
		||||
    "Pause": "विराम",
 | 
			
		||||
    "Name": "नाम",
 | 
			
		||||
    "Message": "संदेश",
 | 
			
		||||
    "No important events": "कोई महत्वपूर्ण घटनाक्रम नहीं",
 | 
			
		||||
    "Edit": "परिवर्तन",
 | 
			
		||||
    "Ping": "पिंग",
 | 
			
		||||
    "Monitor Type": "मॉनिटर प्रकार",
 | 
			
		||||
    "Keyword": "कीवर्ड",
 | 
			
		||||
    "Friendly Name": "दोस्ताना नाम",
 | 
			
		||||
    "Version": "संस्करण",
 | 
			
		||||
    "Home": "घर",
 | 
			
		||||
    "Quick Stats": "शीघ्र आँकड़े",
 | 
			
		||||
    "Reconnecting...": "पुनः कनेक्ट किया जा रहा है...",
 | 
			
		||||
    "Down": "बंद",
 | 
			
		||||
    "Passive Monitor Type": "निष्क्रिय मॉनिटर प्रकार",
 | 
			
		||||
    "Status": "स्थिति"
 | 
			
		||||
}
 | 
			
		||||
@@ -751,5 +751,13 @@
 | 
			
		||||
    "endDateTime": "Data/godzina zakończenia",
 | 
			
		||||
    "cronExpression": "Wyrażenie Cron",
 | 
			
		||||
    "ntfyAuthenticationMethod": "Metoda Uwierzytelnienia",
 | 
			
		||||
    "ntfyUsernameAndPassword": "Nazwa użytkownika i hasło"
 | 
			
		||||
    "ntfyUsernameAndPassword": "Nazwa użytkownika i hasło",
 | 
			
		||||
    "noGroupMonitorMsg": "Niedostępna. Stwórz najpierw grupę monitorów.",
 | 
			
		||||
    "Close": "Zamknij",
 | 
			
		||||
    "pushoverMessageTtl": "TTL wiadomości (sekundy)",
 | 
			
		||||
    "Home": "Strona główna",
 | 
			
		||||
    "Group": "Grupa",
 | 
			
		||||
    "Monitor Group": "Grupa monitora",
 | 
			
		||||
    "Reconnecting...": "Ponowne łączenie...",
 | 
			
		||||
    "Cannot connect to the socket server": "Nie można połączyć się z serwerem gniazda"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
    "upsideDownModeDescription": "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
 | 
			
		||||
    "maxRedirectDescription": "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
 | 
			
		||||
    "acceptedStatusCodesDescription": "Выберите коды статусов для определения доступности сервиса.",
 | 
			
		||||
    "passwordNotMatchMsg": "Повтор пароля не совпадает.",
 | 
			
		||||
    "passwordNotMatchMsg": "Введёные пароли не совпадают",
 | 
			
		||||
    "notificationDescription": "Привяжите уведомления к мониторам.",
 | 
			
		||||
    "keywordDescription": "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру).",
 | 
			
		||||
    "pauseDashboardHome": "Пауза",
 | 
			
		||||
@@ -43,7 +43,7 @@
 | 
			
		||||
    "Delete": "Удалить",
 | 
			
		||||
    "Current": "Текущий",
 | 
			
		||||
    "Uptime": "Аптайм",
 | 
			
		||||
    "Cert Exp.": "Сертификат истекает.",
 | 
			
		||||
    "Cert Exp.": "Сертификат истекает",
 | 
			
		||||
    "day": "день | дней",
 | 
			
		||||
    "-day": "-дней",
 | 
			
		||||
    "hour": "час",
 | 
			
		||||
@@ -69,7 +69,7 @@
 | 
			
		||||
    "Light": "Светлая",
 | 
			
		||||
    "Dark": "Тёмная",
 | 
			
		||||
    "Auto": "Авто",
 | 
			
		||||
    "Theme - Heartbeat Bar": "Тема - Полоса частоты опроса",
 | 
			
		||||
    "Theme - Heartbeat Bar": "Полоса частоты опроса",
 | 
			
		||||
    "Normal": "Обычный",
 | 
			
		||||
    "Bottom": "Снизу",
 | 
			
		||||
    "None": "Отсутствует",
 | 
			
		||||
@@ -160,7 +160,7 @@
 | 
			
		||||
    "Tag with this name already exist.": "Такой тег уже существует.",
 | 
			
		||||
    "Tag with this value already exist.": "Тег с таким значением уже существует.",
 | 
			
		||||
    "color": "цвет",
 | 
			
		||||
    "value (optional)": "значение (опционально)",
 | 
			
		||||
    "value (optional)": "значение (необязательно)",
 | 
			
		||||
    "Gray": "Серый",
 | 
			
		||||
    "Red": "Красный",
 | 
			
		||||
    "Orange": "Оранжевый",
 | 
			
		||||
@@ -175,9 +175,9 @@
 | 
			
		||||
    "Entry Page": "Главная страница",
 | 
			
		||||
    "statusPageNothing": "Здесь пусто. Добавьте группу или монитор.",
 | 
			
		||||
    "No Services": "Нет сервисов",
 | 
			
		||||
    "All Systems Operational": "Все системы работают в штатном режиме",
 | 
			
		||||
    "Partially Degraded Service": "Сервисы работают частично",
 | 
			
		||||
    "Degraded Service": "Все сервисы не работают",
 | 
			
		||||
    "All Systems Operational": "Все системы работают",
 | 
			
		||||
    "Partially Degraded Service": "Частичная работа сервисов",
 | 
			
		||||
    "Degraded Service": "Отказ всех сервисов",
 | 
			
		||||
    "Add Group": "Добавить группу",
 | 
			
		||||
    "Add a monitor": "Добавить монитор",
 | 
			
		||||
    "Edit Status Page": "Редактировать",
 | 
			
		||||
@@ -212,7 +212,7 @@
 | 
			
		||||
    "pushOptionalParams": "Опциональные параметры: {0}",
 | 
			
		||||
    "defaultNotificationName": "Моё уведомление {notification} ({number})",
 | 
			
		||||
    "here": "здесь",
 | 
			
		||||
    "Required": "Требуется",
 | 
			
		||||
    "Required": "Обязательно",
 | 
			
		||||
    "Bot Token": "Токен бота",
 | 
			
		||||
    "wayToGetTelegramToken": "Вы можете взять токен здесь - {0}.",
 | 
			
		||||
    "Chat ID": "ID чата",
 | 
			
		||||
@@ -296,7 +296,7 @@
 | 
			
		||||
    "promosmsPhoneNumber": "Номер телефона (для получателей из Польши можно пропустить код региона)",
 | 
			
		||||
    "promosmsSMSSender": "Имя отправителя SMS: Зарегистрированное или одно из имён по умолчанию: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
 | 
			
		||||
    "Feishu WebHookUrl": "Feishu WebHookURL",
 | 
			
		||||
    "matrixHomeserverURL": "URL сервера (вместе с http(s):// и опционально порт)",
 | 
			
		||||
    "matrixHomeserverURL": "URL сервера (вместе с http(s):// и по желанию порт)",
 | 
			
		||||
    "Internal Room Id": "Внутренний ID комнаты",
 | 
			
		||||
    "matrixDesc1": "Внутренний ID комнаты можно найти в Подробностях в параметрах канала вашего Matrix клиента. Он должен выглядеть примерно как !QMdRCpUIfLwsfjxye6:home.server.",
 | 
			
		||||
    "matrixDesc2": "Рекомендуется создать нового пользователя и не использовать токен доступа личного пользователя Matrix, т.к. это влечёт за собой полный доступ к аккаунту и к комнатам, в которых вы состоите. Вместо этого создайте нового пользователя и пригласите его только в ту комнату, в которой вы хотите получать уведомления. Токен доступа можно получить, выполнив команду {0}",
 | 
			
		||||
@@ -335,9 +335,9 @@
 | 
			
		||||
    "Current User": "Текущий пользователь",
 | 
			
		||||
    "About": "О программе",
 | 
			
		||||
    "Description": "Описание",
 | 
			
		||||
    "Powered by": "Работает на основе скрипта от",
 | 
			
		||||
    "Powered by": "Работает на",
 | 
			
		||||
    "shrinkDatabaseDescription": "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
 | 
			
		||||
    "deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса сервисов?",
 | 
			
		||||
    "deleteStatusPageMsg": "Вы действительно хотите удалить эту страницу статуса?",
 | 
			
		||||
    "Style": "Стиль",
 | 
			
		||||
    "info": "ИНФО",
 | 
			
		||||
    "warning": "ВНИМАНИЕ",
 | 
			
		||||
@@ -367,7 +367,7 @@
 | 
			
		||||
    "Pick Accepted Status Codes...": "Выберите принятые коды состояния…",
 | 
			
		||||
    "Default": "По умолчанию",
 | 
			
		||||
    "Please input title and content": "Пожалуйста, введите название и содержание",
 | 
			
		||||
    "Last Updated": "Последнее Обновление",
 | 
			
		||||
    "Last Updated": "Последнее обновление",
 | 
			
		||||
    "Untitled Group": "Группа без названия",
 | 
			
		||||
    "Services": "Сервисы",
 | 
			
		||||
    "serwersms": "SerwerSMS.pl",
 | 
			
		||||
@@ -379,11 +379,11 @@
 | 
			
		||||
    "smtpDkimSettings": "DKIM Настройки",
 | 
			
		||||
    "smtpDkimDesc": "Пожалуйста ознакомьтесь с {0} Nodemailer DKIM для использования.",
 | 
			
		||||
    "documentation": "документацией",
 | 
			
		||||
    "smtpDkimDomain": "Имя Домена",
 | 
			
		||||
    "smtpDkimDomain": "Имя домена",
 | 
			
		||||
    "smtpDkimKeySelector": "Ключ",
 | 
			
		||||
    "smtpDkimPrivateKey": "Приватный ключ",
 | 
			
		||||
    "smtpDkimHashAlgo": "Алгоритм хэша (опционально)",
 | 
			
		||||
    "smtpDkimheaderFieldNames": "Заголовок ключей для подписи (опционально)",
 | 
			
		||||
    "smtpDkimHashAlgo": "Алгоритм хэша (необязательно)",
 | 
			
		||||
    "smtpDkimheaderFieldNames": "Заголовок ключей для подписи (необязательно)",
 | 
			
		||||
    "smtpDkimskipFields": "Заголовок ключей не для подписи (опционально)",
 | 
			
		||||
    "gorush": "Gorush",
 | 
			
		||||
    "alerta": "Alerta",
 | 
			
		||||
@@ -439,9 +439,9 @@
 | 
			
		||||
    "Uptime Kuma": "Uptime Kuma",
 | 
			
		||||
    "Slug": "Slug",
 | 
			
		||||
    "Accept characters:": "Принимаемые символы:",
 | 
			
		||||
    "startOrEndWithOnly": "Начинается или кончается только {0}",
 | 
			
		||||
    "startOrEndWithOnly": "Начинается или заканчивается только на {0}",
 | 
			
		||||
    "No consecutive dashes": "Без последовательных тире",
 | 
			
		||||
    "The slug is already taken. Please choose another slug.": "Слово уже занято. Пожалуйста, выберите другой вариант.",
 | 
			
		||||
    "The slug is already taken. Please choose another slug.": "Этот slug уже занят. Пожалуйста, выберите другой.",
 | 
			
		||||
    "Page Not Found": "Страница не найдена",
 | 
			
		||||
    "wayToGetCloudflaredURL": "(Скачать cloudflared с {0})",
 | 
			
		||||
    "cloudflareWebsite": "Веб-сайт Cloudflare",
 | 
			
		||||
@@ -467,7 +467,7 @@
 | 
			
		||||
    "onebotMessageType": "Тип сообщения OneBot",
 | 
			
		||||
    "onebotGroupMessage": "Группа",
 | 
			
		||||
    "onebotPrivateMessage": "Private",
 | 
			
		||||
    "onebotUserOrGroupId": "ID группы или пользователя",
 | 
			
		||||
    "onebotUserOrGroupId": "ID группы/пользователя",
 | 
			
		||||
    "onebotSafetyTips": "В целях безопасности необходимо установить токен доступа",
 | 
			
		||||
    "PushDeer Key": "ключ PushDeer",
 | 
			
		||||
    "Footer Text": "Текст нижнего колонтитула",
 | 
			
		||||
@@ -568,7 +568,7 @@
 | 
			
		||||
    "goAlertInfo": "GoAlert — это приложение с открытым исходным кодом для составления расписания вызовов, автоматической эскалации и уведомлений (например, SMS или голосовых звонков). Автоматически привлекайте нужного человека, нужным способом и в нужное время! {0}",
 | 
			
		||||
    "goAlertIntegrationKeyInfo": "Получить общий ключ интеграции API для сервиса в этом формате \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" обычно значение параметра токена скопированного URL.",
 | 
			
		||||
    "goAlert": "GoAlert",
 | 
			
		||||
    "backupOutdatedWarning": "Устарело: поскольку добавлено множество функций, а эта функция резервного копирования немного не поддерживается, она не может создать или восстановить полную резервную копию.",
 | 
			
		||||
    "backupOutdatedWarning": "Устарело: эта функция резервного копирования более не поддерживается. Поскольку добавлено множество функций, она не может создать или восстановить полную резервную копию.",
 | 
			
		||||
    "backupRecommend": "Сделайте резервную копию тома или папки с данными (./data/) напрямую.",
 | 
			
		||||
    "Optional": "Необязательно",
 | 
			
		||||
    "squadcast": "Squadcast",
 | 
			
		||||
@@ -578,24 +578,24 @@
 | 
			
		||||
    "SMSManager": "SMSManager",
 | 
			
		||||
    "You can divide numbers with": "Вы можете делить числа с",
 | 
			
		||||
    "or": "или",
 | 
			
		||||
    "Maintenance": "Обслуживание",
 | 
			
		||||
    "Schedule maintenance": "Запланировать обслуживание",
 | 
			
		||||
    "affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время обслуживания",
 | 
			
		||||
    "affectedStatusPages": "Показывать уведомление об обслуживании на выбранных страницах статуса",
 | 
			
		||||
    "Maintenance": "Техобслуживание",
 | 
			
		||||
    "Schedule maintenance": "Запланировать техобслуживание",
 | 
			
		||||
    "affectedMonitorsDescription": "Выберите мониторы, которые будут затронуты во время техбслуживания",
 | 
			
		||||
    "affectedStatusPages": "Показывать уведомление о техбслуживании на выбранных страницах статуса",
 | 
			
		||||
    "atLeastOneMonitor": "Выберите больше одного затрагиваемого монитора",
 | 
			
		||||
    "dnsPortDescription": "По умолчанию порт DNS сервера - 53. Мы можете изменить его в любое время.",
 | 
			
		||||
    "Monitor": "Монитор | Мониторы",
 | 
			
		||||
    "webhookAdditionalHeadersTitle": "Дополнительные Заголовки",
 | 
			
		||||
    "recurringIntervalMessage": "Запускать 1 раз каждый день | Запускать 1 раз каждые {0} дней",
 | 
			
		||||
    "error": "ошибка",
 | 
			
		||||
    "statusMaintenance": "Обслуживание",
 | 
			
		||||
    "statusMaintenance": "Техобслуживание",
 | 
			
		||||
    "Affected Monitors": "Затронутые мониторы",
 | 
			
		||||
    "Start of maintenance": "Начало обслуживания",
 | 
			
		||||
    "Start of maintenance": "Начало техобслуживания",
 | 
			
		||||
    "All Status Pages": "Все страницы статусов",
 | 
			
		||||
    "Select status pages...": "Выберите страницу статуса…",
 | 
			
		||||
    "resendEveryXTimes": "Повтор каждые {0} раз",
 | 
			
		||||
    "resendDisabled": "Повторная отправка отключена",
 | 
			
		||||
    "deleteMaintenanceMsg": "Вы действительно хотите удалить это обслуживание?",
 | 
			
		||||
    "deleteMaintenanceMsg": "Вы действительно хотите удалить это техбслуживание?",
 | 
			
		||||
    "critical": "критично",
 | 
			
		||||
    "Custom Monitor Type": "Собственный тип монитора",
 | 
			
		||||
    "markdownSupported": "Поддерживает синтаксис Markdown",
 | 
			
		||||
@@ -630,7 +630,7 @@
 | 
			
		||||
    "lastDay2": "Второй последний день месяца",
 | 
			
		||||
    "lastDay3": "Третий последний день месяца",
 | 
			
		||||
    "lastDay4": "Четвертый последний день месяца",
 | 
			
		||||
    "No Maintenance": "Без обслуживания",
 | 
			
		||||
    "No Maintenance": "Нет техбслуживаний",
 | 
			
		||||
    "pauseMaintenanceMsg": "Вы уверены что хотите поставить на паузу?",
 | 
			
		||||
    "maintenanceStatus-under-maintenance": "На техобслуживании",
 | 
			
		||||
    "maintenanceStatus-inactive": "Неактивен",
 | 
			
		||||
@@ -640,13 +640,13 @@
 | 
			
		||||
    "Display Timezone": "Показать часовой пояс",
 | 
			
		||||
    "Server Timezone": "Часовой пояс сервера",
 | 
			
		||||
    "statusPageMaintenanceEndDate": "Конец",
 | 
			
		||||
    "IconUrl": "URL Иконки",
 | 
			
		||||
    "IconUrl": "URL иконки",
 | 
			
		||||
    "Enable DNS Cache": "Включить DNS кэш",
 | 
			
		||||
    "Enable": "Включить",
 | 
			
		||||
    "Disable": "Отключить",
 | 
			
		||||
    "Single Maintenance Window": "Единое Окно Обслуживания",
 | 
			
		||||
    "Schedule Maintenance": "Запланировать обслуживание",
 | 
			
		||||
    "Date and Time": "Дата и Время",
 | 
			
		||||
    "Single Maintenance Window": "Единое окно техбслуживания",
 | 
			
		||||
    "Schedule Maintenance": "Запланировать техбслуживание",
 | 
			
		||||
    "Date and Time": "Дата и время",
 | 
			
		||||
    "DateTime Range": "Промежуток даты и времени",
 | 
			
		||||
    "uninstalling": "Удаляется",
 | 
			
		||||
    "dataRetentionTimeError": "Период хранения должен быть равен 0 или больше",
 | 
			
		||||
@@ -676,10 +676,10 @@
 | 
			
		||||
    "Integration URL": "URL интеграции",
 | 
			
		||||
    "do nothing": "ничего не делать",
 | 
			
		||||
    "smseagleTo": "Номер(а) телефона",
 | 
			
		||||
    "smseagleGroup": "Название(я) групп телефонной книги",
 | 
			
		||||
    "smseagleContact": "Имена контактов из телефонной книжки",
 | 
			
		||||
    "smseagleGroup": "Название(я) группы телефонной книги",
 | 
			
		||||
    "smseagleContact": "Имена контактов телефонной книги",
 | 
			
		||||
    "smseagleRecipientType": "Тип получателя",
 | 
			
		||||
    "smseagleRecipient": "Получатель(я) (через запятую, если необходимо указать несколько)",
 | 
			
		||||
    "smseagleRecipient": "Получатель(и) (если множество, должны быть разделены запятой)",
 | 
			
		||||
    "smseagleToken": "Токен доступа API",
 | 
			
		||||
    "smseagleUrl": "URL вашего SMSEagle устройства",
 | 
			
		||||
    "smseagleEncoding": "Отправить в юникоде",
 | 
			
		||||
@@ -695,7 +695,7 @@
 | 
			
		||||
    "telegramProtectContentDescription": "Если включено, сообщения бота в Telegram будут запрещены для пересылки и сохранения.",
 | 
			
		||||
    "telegramSendSilently": "Отправить без звука",
 | 
			
		||||
    "telegramSendSilentlyDescription": "Пользователи получат уведомление без звука.",
 | 
			
		||||
    "Maintenance Time Window of a Day": "Суточный интервал для обслуживания",
 | 
			
		||||
    "Maintenance Time Window of a Day": "Суточный интервал для техбслуживания",
 | 
			
		||||
    "Clone Monitor": "Копия",
 | 
			
		||||
    "Clone": "Копия",
 | 
			
		||||
    "cloneOf": "Копия {0}",
 | 
			
		||||
@@ -703,31 +703,31 @@
 | 
			
		||||
    "Add New Tag": "Добавить тег",
 | 
			
		||||
    "Body Encoding": "Тип содержимого запроса.(JSON or XML)",
 | 
			
		||||
    "Strategy": "Стратегия",
 | 
			
		||||
    "Free Mobile User Identifier": "Бесплатный идентификатор мобильного пользователя",
 | 
			
		||||
    "Free Mobile User Identifier": "Бесплатный мобильный идентификатор пользователя",
 | 
			
		||||
    "Auto resolve or acknowledged": "Автоматическое разрешение или подтверждение",
 | 
			
		||||
    "auto acknowledged": "автоматическое подтверждение",
 | 
			
		||||
    "auto resolve": "автоматическое разрешение",
 | 
			
		||||
    "API Keys": "Ключи API",
 | 
			
		||||
    "Expiry": "Истекает",
 | 
			
		||||
    "Expiry date": "Дата окончания действия",
 | 
			
		||||
    "Expiry": "Срок действия",
 | 
			
		||||
    "Expiry date": "Дата истечения срока действия",
 | 
			
		||||
    "Don't expire": "Не истекает",
 | 
			
		||||
    "Continue": "Продолжать",
 | 
			
		||||
    "Add Another": "Добавьте еще один",
 | 
			
		||||
    "Continue": "Продолжить",
 | 
			
		||||
    "Add Another": "Добавить еще",
 | 
			
		||||
    "Key Added": "Ключ добавлен",
 | 
			
		||||
    "Add API Key": "Добавить ключ API",
 | 
			
		||||
    "No API Keys": "Нет API ключей",
 | 
			
		||||
    "Add API Key": "Добавить API ключ",
 | 
			
		||||
    "No API Keys": "Нет ключей API",
 | 
			
		||||
    "apiKey-active": "Активный",
 | 
			
		||||
    "apiKey-expired": "Истёк",
 | 
			
		||||
    "apiKey-inactive": "Неактивный",
 | 
			
		||||
    "Expires": "Истекает",
 | 
			
		||||
    "disableAPIKeyMsg": "Вы уверены, что хотите отключить этот ключ?",
 | 
			
		||||
    "disableAPIKeyMsg": "Вы уверены, что хотите отключить этот API ключ?",
 | 
			
		||||
    "Generate": "Сгенерировать",
 | 
			
		||||
    "pagertreeResolve": "Автоматическое разрешение",
 | 
			
		||||
    "pagertreeDoNothing": "ничего не делать",
 | 
			
		||||
    "pagertreeDoNothing": "Ничего не делать",
 | 
			
		||||
    "lunaseaTarget": "Цель",
 | 
			
		||||
    "lunaseaDeviceID": "Идентификатор устройства",
 | 
			
		||||
    "lunaseaUserID": "Идентификатор пользователя",
 | 
			
		||||
    "Lowcost": "Низкая стоимость",
 | 
			
		||||
    "Lowcost": "Бюджетный",
 | 
			
		||||
    "pagertreeIntegrationUrl": "URL-адрес интеграции",
 | 
			
		||||
    "pagertreeUrgency": "Срочность",
 | 
			
		||||
    "pagertreeSilent": "Тихий",
 | 
			
		||||
@@ -736,15 +736,15 @@
 | 
			
		||||
    "pagertreeHigh": "Высокий",
 | 
			
		||||
    "pagertreeCritical": "Критический",
 | 
			
		||||
    "high": "высокий",
 | 
			
		||||
    "promosmsAllowLongSMS": "Разрешить длинные SMS-сообщения",
 | 
			
		||||
    "promosmsAllowLongSMS": "Разрешить длинные СМС",
 | 
			
		||||
    "Economy": "Экономия",
 | 
			
		||||
    "wayToGetPagerDutyKey": "Вы можете получить это, перейдя в службу -> Каталог служб -> (Выберите службу) -> Интеграции -> Добавить интеграцию. Здесь вы можете выполнить поиск по \"Events API V2\". Дополнительная информация {0}",
 | 
			
		||||
    "apiKeyAddedMsg": "Ваш API ключ был добавлен. Пожалуйста, запишите это, так как оно больше не будет показан.",
 | 
			
		||||
    "wayToGetPagerDutyKey": "Вы можете это получить, перейдя в Сервис -> Каталог сервисов -> (Выберите сервис) -> Интеграции -> Добавить интеграцию. Здесь вы можете искать «Events API V2». Подробнее {0}",
 | 
			
		||||
    "apiKeyAddedMsg": "Ваш ключ API добавлен. Пожалуйста, обратите внимание на это сообщение, так как оно отображается один раз.",
 | 
			
		||||
    "deleteAPIKeyMsg": "Вы уверены, что хотите удалить этот ключ API?",
 | 
			
		||||
    "wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree, скопируйте конечную точку. Смотрите полную информацию {0}",
 | 
			
		||||
    "wayToGetPagerTreeIntegrationURL": "После создания интеграции Uptime Kuma в PagerTree скопируйте файл Endpoint. См. полную информацию {0}",
 | 
			
		||||
    "telegramMessageThreadIDDescription": "Необязательный уникальный идентификатор для цепочки сообщений (темы) форума; только для форумов-супергрупп",
 | 
			
		||||
    "grpcMethodDescription": "Название метода - преобразовать в формат cammelCase, такой как sayHello, check и т.д.",
 | 
			
		||||
    "Proto Service Name": "название службы Proto",
 | 
			
		||||
    "grpcMethodDescription": "Имя метода преобразуется в формат cammelCase, например, sayHello, check и т. д.",
 | 
			
		||||
    "Proto Service Name": "Название службы Proto",
 | 
			
		||||
    "Proto Method": "Метод Proto",
 | 
			
		||||
    "Proto Content": "Содержание Proto",
 | 
			
		||||
    "telegramMessageThreadID": "(Необязательно) ID цепочки сообщений",
 | 
			
		||||
@@ -758,5 +758,40 @@
 | 
			
		||||
    "endDateTime": "Конечная дата и время",
 | 
			
		||||
    "cronExpression": "Выражение для Cron",
 | 
			
		||||
    "cronSchedule": "Расписание: ",
 | 
			
		||||
    "invalidCronExpression": "Неверное выражение Cron: {0}"
 | 
			
		||||
    "invalidCronExpression": "Неверное выражение Cron: {0}",
 | 
			
		||||
    "ntfyUsernameAndPassword": "Логин и пароль",
 | 
			
		||||
    "ntfyAuthenticationMethod": "Способ входа",
 | 
			
		||||
    "Monitor Setting": "Настройка монитора {0}",
 | 
			
		||||
    "Show Clickable Link": "Показать кликабельную ссылку",
 | 
			
		||||
    "Badge Generator": "Генератор значков для {0}",
 | 
			
		||||
    "Badge Type": "Тип значка",
 | 
			
		||||
    "Badge Duration": "Срок действия значка",
 | 
			
		||||
    "Badge Label": "Надпись для значка",
 | 
			
		||||
    "Badge Prefix": "Префикс значка",
 | 
			
		||||
    "Badge Label Color": "Цвет надписи значка",
 | 
			
		||||
    "Badge Color": "Цвет значка",
 | 
			
		||||
    "Badge Label Prefix": "Префикс надписи для значка",
 | 
			
		||||
    "Open Badge Generator": "Открыть генератор значка",
 | 
			
		||||
    "Badge Up Color": "Цвет значка для статуса \"Доступен\"",
 | 
			
		||||
    "Badge Pending Color": "Цвет значка для статуса \"Ожидание\"",
 | 
			
		||||
    "Badge Maintenance Color": "Цвет значка для статуса \"Техобслуживание\"",
 | 
			
		||||
    "Badge Style": "Стиль значка",
 | 
			
		||||
    "Badge Suffix": "Суффикс значка",
 | 
			
		||||
    "Badge value (For Testing only.)": "Значение значка (только для тестирования)",
 | 
			
		||||
    "Badge URL": "URL значка",
 | 
			
		||||
    "Group": "Группа",
 | 
			
		||||
    "Monitor Group": "Группа мониторов",
 | 
			
		||||
    "Show Clickable Link Description": "Если флажок установлен, все, кто имеет доступ к этой странице состояния, могут иметь доступ к URL-адресу монитора.",
 | 
			
		||||
    "pushoverMessageTtl": "TTL сообщения (в секундах)",
 | 
			
		||||
    "Badge Down Color": "Цвет значка для статуса \"Недоступен\"",
 | 
			
		||||
    "Badge Label Suffix": "Суффикс надписи для значка",
 | 
			
		||||
    "Edit Maintenance": "Редактировать техобсоуживание",
 | 
			
		||||
    "Reconnecting...": "Переподключение...",
 | 
			
		||||
    "Cannot connect to the socket server": "Невозможно подключиться к серверу",
 | 
			
		||||
    "Badge Warn Color": "Цвет значка для предупреждения",
 | 
			
		||||
    "Badge Warn Days": "Значок для \"дней предупреждения\"",
 | 
			
		||||
    "Badge Down Days": "Значок для \"дней недоступности\"",
 | 
			
		||||
    "Home": "Главная",
 | 
			
		||||
    "noGroupMonitorMsg": "Не доступно. Создайте сначала группу мониторов.",
 | 
			
		||||
    "Close": "Закрыть"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -214,7 +214,7 @@
 | 
			
		||||
    "smtpBCC": "BCC",
 | 
			
		||||
    "discord": "Discord",
 | 
			
		||||
    "Discord Webhook URL": "Discord Webhook URL",
 | 
			
		||||
    "wayToGetDiscordURL": "คุณสามารถรับได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
 | 
			
		||||
    "wayToGetDiscordURL": "คุณสามารถทำได้โดยการไปที่ Server Settings -> Integrations -> Create Webhook",
 | 
			
		||||
    "Bot Display Name": "ชื่อบอท",
 | 
			
		||||
    "Prefix Custom Message": "คำนำหน้าข้อความที่กำหนดเอง",
 | 
			
		||||
    "Hello @everyone is...": "สวัสดี {'@'}everyone นี่…",
 | 
			
		||||
@@ -652,5 +652,23 @@
 | 
			
		||||
    "Enable DNS Cache": "เปิดใช้งาน DNS Cache",
 | 
			
		||||
    "Enable": "เปิดใช้งาน",
 | 
			
		||||
    "Disable": "ปิดใช้งาน",
 | 
			
		||||
    "Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว"
 | 
			
		||||
    "Single Maintenance Window": "หน้าการปรับปรุงเดี่ยว",
 | 
			
		||||
    "Clone Monitor": "มอนิเตอร์",
 | 
			
		||||
    "Clone": "โคลนมอนิเตอร์",
 | 
			
		||||
    "cloneOf": "ชื่อเล่นมอนิเตอร์",
 | 
			
		||||
    "wayToGetZohoCliqURL": "คุณสามารถดูวิธีการสร้าง Webhook URL {0}",
 | 
			
		||||
    "Cannot connect to the socket server": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Socket",
 | 
			
		||||
    "Reconnecting...": "กำลังเชื่อมต่อใหม่",
 | 
			
		||||
    "Home": "หน้าหลัก",
 | 
			
		||||
    "Date and Time": "วันที่และเวลา",
 | 
			
		||||
    "DateTime Range": "ช่วงวันที่และเวลา",
 | 
			
		||||
    "loadingError": "ไม่สามารถดึงข้อมูลได้ โปรดลองอีกครั้งในภายหลัง",
 | 
			
		||||
    "plugin": "ปลั้กอิน | ปลั้กอิน",
 | 
			
		||||
    "install": "ติดตั้ง",
 | 
			
		||||
    "installing": "กำลังติดตั้ง",
 | 
			
		||||
    "uninstall": "ถอนการติดตั้ง",
 | 
			
		||||
    "uninstalling": "กำลังถอนการติดตั้ง",
 | 
			
		||||
    "confirmUninstallPlugin": "แน่ใจหรือไม่ว่าต้องการถอนการติดตั้งปลั้กอินนี้?",
 | 
			
		||||
    "Schedule Maintenance": "กำหนดเวลาซ่อมแซม",
 | 
			
		||||
    "Edit Maintenance": "แก้ใขการบำรุงรักษา"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -776,5 +776,13 @@
 | 
			
		||||
    "Badge value (For Testing only.)": "Rozet değeri (Yalnızca Test için.)",
 | 
			
		||||
    "Badge URL": "Rozet URL'i",
 | 
			
		||||
    "Monitor Setting": "{0}'nin Monitör Ayarı",
 | 
			
		||||
    "Show Clickable Link Description": "Eğer işaretlenirse, bu durum sayfasına erişimi olan herkes monitor URL'ine erişebilir."
 | 
			
		||||
    "Show Clickable Link Description": "Eğer işaretlenirse, bu durum sayfasına erişimi olan herkes monitor URL'ine erişebilir.",
 | 
			
		||||
    "Group": "Grup",
 | 
			
		||||
    "Monitor Group": "Monitor Grup",
 | 
			
		||||
    "Cannot connect to the socket server": "Soket sunucusuna bağlanılamıyor",
 | 
			
		||||
    "Edit Maintenance": "Bakımı Düzenle",
 | 
			
		||||
    "Reconnecting...": "Yeniden bağlanılıyor...",
 | 
			
		||||
    "Home": "Anasayfa",
 | 
			
		||||
    "noGroupMonitorMsg": "Uygun değil. Önce bir Grup Monitörü oluşturun.",
 | 
			
		||||
    "Close": "Kapalı"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -462,7 +462,7 @@
 | 
			
		||||
    "onebotMessageType": "OneBot тип повідомлення",
 | 
			
		||||
    "onebotGroupMessage": "Група",
 | 
			
		||||
    "onebotPrivateMessage": "Приватне",
 | 
			
		||||
    "onebotUserOrGroupId": "Група/Користувач ID",
 | 
			
		||||
    "onebotUserOrGroupId": "Група/ID користувача",
 | 
			
		||||
    "onebotSafetyTips": "Для безпеки необхідно встановити маркер доступу",
 | 
			
		||||
    "PushDeer Key": "PushDeer ключ",
 | 
			
		||||
    "Footer Text": "Текст нижнього колонтитула",
 | 
			
		||||
@@ -782,5 +782,13 @@
 | 
			
		||||
    "Badge Warn Color": "Колір бейджа \"Попередження\"",
 | 
			
		||||
    "Badge Warn Days": "Бейдж \"Днів попередження\"",
 | 
			
		||||
    "Badge Maintenance Color": "Колір бейджа \"Обслуговування\"",
 | 
			
		||||
    "Badge Down Days": "Бейдж \"Днів недоступний\""
 | 
			
		||||
    "Badge Down Days": "Бейдж \"Днів недоступний\"",
 | 
			
		||||
    "Group": "Група",
 | 
			
		||||
    "Monitor Group": "Група моніторів",
 | 
			
		||||
    "Edit Maintenance": "Редагувати обслуговування",
 | 
			
		||||
    "Cannot connect to the socket server": "Не вдається підключитися до сервера сокетів",
 | 
			
		||||
    "Reconnecting...": "Повторне підключення...",
 | 
			
		||||
    "Home": "Головна",
 | 
			
		||||
    "noGroupMonitorMsg": "Недоступно. Спочатку створіть групу моніторів.",
 | 
			
		||||
    "Close": "Закрити"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -528,8 +528,8 @@
 | 
			
		||||
    "RadiusCallingStationId": "呼叫方号码(Calling Station Id)",
 | 
			
		||||
    "RadiusCallingStationIdDescription": "发出请求的设备的标识",
 | 
			
		||||
    "Certificate Expiry Notification": "证书到期时通知",
 | 
			
		||||
    "API Username": "API  用户名",
 | 
			
		||||
    "API Key": "API  密钥",
 | 
			
		||||
    "API Username": "API 用户名",
 | 
			
		||||
    "API Key": "API 密钥",
 | 
			
		||||
    "Recipient Number": "收件人手机号码",
 | 
			
		||||
    "From Name/Number": "发件人名称/手机号码",
 | 
			
		||||
    "Leave blank to use a shared sender number.": "留空以使用平台共享的发件人手机号码。",
 | 
			
		||||
@@ -778,5 +778,13 @@
 | 
			
		||||
    "Badge Label Prefix": "徽章标签前缀",
 | 
			
		||||
    "Badge Label Color": "徽章标签颜色",
 | 
			
		||||
    "Show Clickable Link Description": "勾选后所有能访问本状态页的访客均可查看该监控项网址。",
 | 
			
		||||
    "Show Clickable Link": "显示可点击的监控项链接"
 | 
			
		||||
    "Show Clickable Link": "显示可点击的监控项链接",
 | 
			
		||||
    "Group": "组",
 | 
			
		||||
    "Monitor Group": "监控项组",
 | 
			
		||||
    "Cannot connect to the socket server": "无法连接到后端服务器",
 | 
			
		||||
    "Reconnecting...": "重连中……",
 | 
			
		||||
    "Edit Maintenance": "编辑维护计划",
 | 
			
		||||
    "Home": "首页",
 | 
			
		||||
    "noGroupMonitorMsg": "暂无可用,请先创建一个监控项组。",
 | 
			
		||||
    "Close": "关闭"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -706,5 +706,43 @@
 | 
			
		||||
    "wayToGetKookBotToken": "到 {0} 創建應用程式並取得 bot token",
 | 
			
		||||
    "dataRetentionTimeError": "保留期限必須為 0 或正數",
 | 
			
		||||
    "infiniteRetention": "設定為 0 以作無限期保留。",
 | 
			
		||||
    "confirmDeleteTagMsg": "你確定你要刪除此標籤?相關的監測器不會被刪除。"
 | 
			
		||||
    "confirmDeleteTagMsg": "你確定你要刪除此標籤?相關的監測器不會被刪除。",
 | 
			
		||||
    "twilioAuthToken": "認證 Token",
 | 
			
		||||
    "twilioAccountSID": "帳號 SID",
 | 
			
		||||
    "ntfyUsernameAndPassword": "使用者名稱和密碼",
 | 
			
		||||
    "ntfyAuthenticationMethod": "認證類型",
 | 
			
		||||
    "API Keys": "API 金鑰",
 | 
			
		||||
    "Expiry": "到期",
 | 
			
		||||
    "apiKey-inactive": "無效",
 | 
			
		||||
    "apiKey-expired": "過期",
 | 
			
		||||
    "Reconnecting...": "重新連線...",
 | 
			
		||||
    "Expiry date": "到期時間",
 | 
			
		||||
    "Don't expire": "不要過期",
 | 
			
		||||
    "Continue": "繼續",
 | 
			
		||||
    "Add Another": "新增作者",
 | 
			
		||||
    "Add API Key": "新增 API 金鑰",
 | 
			
		||||
    "Generate": "產生",
 | 
			
		||||
    "lunaseaTarget": "目標",
 | 
			
		||||
    "lunaseaDeviceID": "裝置 ID",
 | 
			
		||||
    "lunaseaUserID": "使用者 ID",
 | 
			
		||||
    "Cannot connect to the socket server": "無法連線到 Socket 伺服器",
 | 
			
		||||
    "Edit Maintenance": "編輯維護",
 | 
			
		||||
    "deleteAPIKeyMsg": "您確定要刪除這個 API 金鑰?",
 | 
			
		||||
    "Custom Monitor Type": "自訂監視器類型",
 | 
			
		||||
    "Google Analytics ID": "Google Analytics ID",
 | 
			
		||||
    "Server Address": "伺服器位置",
 | 
			
		||||
    "Edit Tag": "編輯標籤",
 | 
			
		||||
    "pagertreeMedium": "中",
 | 
			
		||||
    "pagertreeHigh": "高",
 | 
			
		||||
    "pagertreeResolve": "自動解決",
 | 
			
		||||
    "pagertreeLow": "低",
 | 
			
		||||
    "Learn More": "閱讀更多",
 | 
			
		||||
    "pushoverMessageTtl": "Message TTL (秒)",
 | 
			
		||||
    "apiKeyAddedMsg": "您的 API 金鑰已建立。金鑰不會再次顯示,請將它放在安全的地方。",
 | 
			
		||||
    "No API Keys": "無 API 金鑰",
 | 
			
		||||
    "apiKey-active": "活躍",
 | 
			
		||||
    "Expires": "過期",
 | 
			
		||||
    "disableAPIKeyMsg": "您確定要停用這個 API 金鑰?",
 | 
			
		||||
    "Monitor Setting": "{0} 的監視器設定",
 | 
			
		||||
    "Guild ID": "Guild ID"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,9 @@ export default {
 | 
			
		||||
        theme() {
 | 
			
		||||
            // As entry can be status page now, set forceStatusPageTheme to true to use status page theme
 | 
			
		||||
            if (this.forceStatusPageTheme) {
 | 
			
		||||
                if (this.statusPageTheme === "auto") {
 | 
			
		||||
                    return this.system;
 | 
			
		||||
                }
 | 
			
		||||
                return this.statusPageTheme;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,20 @@
 | 
			
		||||
                <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <p class="url">
 | 
			
		||||
                <a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
 | 
			
		||||
                <a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
 | 
			
		||||
                <span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
 | 
			
		||||
                <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
 | 
			
		||||
                <span v-if="monitor.type === 'keyword'">
 | 
			
		||||
                    <br>
 | 
			
		||||
                    <span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
 | 
			
		||||
                    <span>{{ $t("Keyword") }}: </span>
 | 
			
		||||
                    <span class="keyword">{{ monitor.keyword }}</span>
 | 
			
		||||
                    <span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> ↧</span>
 | 
			
		||||
                </span>
 | 
			
		||||
                <span v-if="monitor.type === 'json-query'">
 | 
			
		||||
                    <br>
 | 
			
		||||
                    <span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    <span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
 | 
			
		||||
                </span>
 | 
			
		||||
                <span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
 | 
			
		||||
                    <br>
 | 
			
		||||
@@ -68,6 +76,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Stats -->
 | 
			
		||||
            <div class="shadow-box big-padding text-center stats">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
 | 
			
		||||
@@ -131,6 +140,15 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Screenshot -->
 | 
			
		||||
            <div v-if="monitor.type === 'real-browser'" class="shadow-box">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <img :src="screenshotURL" alt style="width: 100%;">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="shadow-box table-shadow-box">
 | 
			
		||||
                <div class="dropdown dropdown-clear-data">
 | 
			
		||||
                    <button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
 | 
			
		||||
@@ -217,6 +235,7 @@ import Tag from "../components/Tag.vue";
 | 
			
		||||
import CertificateInfo from "../components/CertificateInfo.vue";
 | 
			
		||||
import { getMonitorRelativeURL } from "../util.ts";
 | 
			
		||||
import { URL } from "whatwg-url";
 | 
			
		||||
import { getResBaseURL } from "../util-frontend";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
@@ -242,6 +261,7 @@ export default {
 | 
			
		||||
                hideCount: true,
 | 
			
		||||
                chunksNavigation: "scroll",
 | 
			
		||||
            },
 | 
			
		||||
            cacheTime: Date.now(),
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
@@ -251,6 +271,10 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        lastHeartBeat() {
 | 
			
		||||
            // Also trigger screenshot refresh here
 | 
			
		||||
            // eslint-disable-next-line vue/no-side-effects-in-computed-properties
 | 
			
		||||
            this.cacheTime = Date.now();
 | 
			
		||||
 | 
			
		||||
            if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
 | 
			
		||||
                return this.$root.lastHeartbeatList[this.monitor.id];
 | 
			
		||||
            }
 | 
			
		||||
@@ -325,11 +349,16 @@ export default {
 | 
			
		||||
        pushURL() {
 | 
			
		||||
            return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        screenshotURL() {
 | 
			
		||||
            return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        getResBaseURL,
 | 
			
		||||
        /** Request a test notification be sent for this monitor */
 | 
			
		||||
        testNotification() {
 | 
			
		||||
            this.$root.getSocket().emit("testNotification", this.monitor.id);
 | 
			
		||||
@@ -411,7 +440,7 @@ export default {
 | 
			
		||||
                translationPrefix = "Avg. ";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.monitor.type === "http" || this.monitor.type === "keyword") {
 | 
			
		||||
            if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") {
 | 
			
		||||
                return this.$t(translationPrefix + "Response");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -561,6 +590,10 @@ table {
 | 
			
		||||
        color: $dark-font-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .keyword-inverted {
 | 
			
		||||
        color: $dark-font-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropdown-clear-data {
 | 
			
		||||
        ul {
 | 
			
		||||
            background-color: $dark-bg;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,9 @@
 | 
			
		||||
                                        <option value="keyword">
 | 
			
		||||
                                            HTTP(s) - {{ $t("Keyword") }}
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                        <option value="json-query">
 | 
			
		||||
                                            HTTP(s) - {{ $t("Json Query") }}
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                        <option value="grpc-keyword">
 | 
			
		||||
                                            gRPC(s) - {{ $t("Keyword") }}
 | 
			
		||||
                                        </option>
 | 
			
		||||
@@ -36,6 +39,10 @@
 | 
			
		||||
                                        <option value="docker">
 | 
			
		||||
                                            {{ $t("Docker Container") }}
 | 
			
		||||
                                        </option>
 | 
			
		||||
 | 
			
		||||
                                        <option value="real-browser">
 | 
			
		||||
                                            HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    </optgroup>
 | 
			
		||||
 | 
			
		||||
                                    <optgroup :label="$t('Passive Monitor Type')">
 | 
			
		||||
@@ -73,16 +80,6 @@
 | 
			
		||||
                                            Redis
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    </optgroup>
 | 
			
		||||
 | 
			
		||||
                                    <!--
 | 
			
		||||
                                    Hidden for now: Reason refer to Setting.vue
 | 
			
		||||
                                    <optgroup :label="$t('Custom Monitor Type')">
 | 
			
		||||
                                        <option value="browser">
 | 
			
		||||
                                            (Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    </optgroup>
 | 
			
		||||
                                </select>
 | 
			
		||||
                                -->
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
@@ -103,7 +100,7 @@
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- URL -->
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
 | 
			
		||||
                                <label for="url" class="form-label">{{ $t("URL") }}</label>
 | 
			
		||||
                                <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
@@ -133,6 +130,31 @@
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Invert keyword -->
 | 
			
		||||
                            <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3 form-check">
 | 
			
		||||
                                <input id="invert-keyword" v-model="monitor.invertKeyword" class="form-check-input" type="checkbox">
 | 
			
		||||
                                <label class="form-check-label" for="invert-keyword">
 | 
			
		||||
                                    {{ $t("Invert Keyword") }}
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <div class="form-text">
 | 
			
		||||
                                    {{ $t("invertKeywordDescription") }}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Json Query -->
 | 
			
		||||
                            <div v-if="monitor.type === 'json-query'" class="my-3">
 | 
			
		||||
                                <label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
 | 
			
		||||
                                <input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
 | 
			
		||||
 | 
			
		||||
                                <!-- eslint-disable-next-line vue/no-v-html -->
 | 
			
		||||
                                <div class="form-text" v-html="$t('jsonQueryDescription')">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <br>
 | 
			
		||||
 | 
			
		||||
                                <label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
 | 
			
		||||
                                <input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Game -->
 | 
			
		||||
                            <!-- GameDig only -->
 | 
			
		||||
                            <div v-if="monitor.type === 'gamedig'" class="my-3">
 | 
			
		||||
@@ -362,7 +384,7 @@
 | 
			
		||||
 | 
			
		||||
                            <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
 | 
			
		||||
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
 | 
			
		||||
                                <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
 | 
			
		||||
                                <label class="form-check-label" for="expiry-notification">
 | 
			
		||||
                                    {{ $t("Certificate Expiry Notification") }}
 | 
			
		||||
@@ -371,7 +393,7 @@
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
 | 
			
		||||
                                <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
 | 
			
		||||
                                <label class="form-check-label" for="ignore-tls">
 | 
			
		||||
                                    {{ $t("ignoreTLSError") }}
 | 
			
		||||
@@ -463,7 +485,7 @@
 | 
			
		||||
                            </button>
 | 
			
		||||
 | 
			
		||||
                            <!-- Proxies -->
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword'">
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'">
 | 
			
		||||
                                <h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
 | 
			
		||||
                                <p v-if="$root.proxyList.length === 0">
 | 
			
		||||
                                    {{ $t("Not available, please setup.") }}
 | 
			
		||||
@@ -491,7 +513,7 @@
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- HTTP Options -->
 | 
			
		||||
                            <template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
 | 
			
		||||
                            <template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' ">
 | 
			
		||||
                                <h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
 | 
			
		||||
 | 
			
		||||
                                <!-- Method -->
 | 
			
		||||
@@ -1113,7 +1135,7 @@ message HealthCheckResponse {
 | 
			
		||||
                this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") {
 | 
			
		||||
            if (this.monitor.type && this.monitor.type !== "http" && (this.monitor.type !== "keyword" || this.monitor.type !== "json-query")) {
 | 
			
		||||
                this.monitor.httpBodyEncoding = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -116,12 +116,6 @@ export default {
 | 
			
		||||
                backup: {
 | 
			
		||||
                    title: this.$t("Backup"),
 | 
			
		||||
                },
 | 
			
		||||
                /*
 | 
			
		||||
                Hidden for now: Unfortunately, after some test, I found that Playwright requires a lot of libraries to be installed on the Linux host in order to start Chrome or Firefox.
 | 
			
		||||
                It will be hard to install, so I hide this feature for now. But it still accessible via URL: /settings/plugins.
 | 
			
		||||
                plugins: {
 | 
			
		||||
                    title: this.$tc("plugin", 2),
 | 
			
		||||
                },*/
 | 
			
		||||
                about: {
 | 
			
		||||
                    title: this.$t("About"),
 | 
			
		||||
                },
 | 
			
		||||
 
 | 
			
		||||
@@ -325,7 +325,7 @@
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <div class="refresh-info mb-2">
 | 
			
		||||
                    <div>{{ $t("Last Updated") }}: <date-time :value="lastUpdateTime" /></div>
 | 
			
		||||
                    <div>{{ $t("Last Updated") }}:  {{ lastUpdateTimeDisplay }}</div>
 | 
			
		||||
                    <div>{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </footer>
 | 
			
		||||
@@ -360,7 +360,6 @@ import DOMPurify from "dompurify";
 | 
			
		||||
import Confirm from "../components/Confirm.vue";
 | 
			
		||||
import PublicGroupList from "../components/PublicGroupList.vue";
 | 
			
		||||
import MaintenanceTime from "../components/MaintenanceTime.vue";
 | 
			
		||||
import DateTime from "../components/Datetime.vue";
 | 
			
		||||
import { getResBaseURL } from "../util-frontend";
 | 
			
		||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
 | 
			
		||||
import Tag from "../components/Tag.vue";
 | 
			
		||||
@@ -386,7 +385,6 @@ export default {
 | 
			
		||||
        Confirm,
 | 
			
		||||
        PrismEditor,
 | 
			
		||||
        MaintenanceTime,
 | 
			
		||||
        DateTime,
 | 
			
		||||
        Tag,
 | 
			
		||||
        VueMultiselect
 | 
			
		||||
    },
 | 
			
		||||
@@ -583,6 +581,10 @@ export default {
 | 
			
		||||
                return "";
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        lastUpdateTimeDisplay() {
 | 
			
		||||
            return this.$root.datetime(this.lastUpdateTime);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ import DockerHosts from "./components/settings/Docker.vue";
 | 
			
		||||
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
 | 
			
		||||
import ManageMaintenance from "./pages/ManageMaintenance.vue";
 | 
			
		||||
import APIKeys from "./components/settings/APIKeys.vue";
 | 
			
		||||
import Plugins from "./components/settings/Plugins.vue";
 | 
			
		||||
 | 
			
		||||
// Settings - Sub Pages
 | 
			
		||||
import Appearance from "./components/settings/Appearance.vue";
 | 
			
		||||
@@ -130,10 +129,6 @@ const routes = [
 | 
			
		||||
                                path: "backup",
 | 
			
		||||
                                component: Backup,
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                path: "plugins",
 | 
			
		||||
                                component: Plugins,
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                path: "about",
 | 
			
		||||
                                component: About,
 | 
			
		||||
 
 | 
			
		||||
@@ -306,6 +306,16 @@ describe("Test uptimeKumaServer.getClientIP()", () => {
 | 
			
		||||
        ip = await server.getClientIP(fakeSocket);
 | 
			
		||||
        expect(ip).toBe("203.0.113.195");
 | 
			
		||||
 | 
			
		||||
        fakeSocket.client.conn.remoteAddress = "2001:db8::1";
 | 
			
		||||
        fakeSocket.client.conn.request.headers = {};
 | 
			
		||||
        ip = await server.getClientIP(fakeSocket);
 | 
			
		||||
        expect(ip).toBe("2001:db8::1");
 | 
			
		||||
 | 
			
		||||
        fakeSocket.client.conn.remoteAddress = "::ffff:127.0.0.1";
 | 
			
		||||
        fakeSocket.client.conn.request.headers = {};
 | 
			
		||||
        ip = await server.getClientIP(fakeSocket);
 | 
			
		||||
        expect(ip).toBe("127.0.0.1");
 | 
			
		||||
 | 
			
		||||
        await Database.close();
 | 
			
		||||
    }, 120000);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user