mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-01 03:49:24 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into 2.0.X
# Conflicts: # docker/debian-base.dockerfile # package-lock.json # package.json # server/database.js # src/router.js
This commit is contained in:
		
							
								
								
									
										28
									
								
								.devcontainer/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.devcontainer/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Codespaces | ||||
|  | ||||
| You can modifiy Uptime Kuma in your browser without setting up a local development. | ||||
|  | ||||
|  | ||||
|  | ||||
| 1. Click `Code` -> `Create codespace on master` | ||||
| 2. Wait a few minutes until you see there are two exposed ports | ||||
| 3. Go to the `3000` url, see if it is working  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Frontend | ||||
|  | ||||
| Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded. | ||||
| You don't need to restart the frontend, unless you try to add a new frontend dependency. | ||||
|  | ||||
| ## Backend | ||||
|  | ||||
| The backend does not automatically hot-reload. | ||||
| You will need to restart the backend after changing something using these steps: | ||||
|  | ||||
| 1. Click `Terminal` | ||||
| 2. Click `Codespaces: server-dev` in the right panel | ||||
| 3. Press `Ctrl + C` to stop the server | ||||
| 4. Press `Up` to run `npm run start-server-dev` | ||||
|  | ||||
|  | ||||
							
								
								
									
										22
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|     "image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm", | ||||
|     "features": { | ||||
|         "ghcr.io/devcontainers/features/github-cli:1": {} | ||||
|     }, | ||||
|     "updateContentCommand": "npm ci", | ||||
|     "postCreateCommand": "", | ||||
|     "postAttachCommand": { | ||||
|         "frontend-dev": "npm run start-frontend-devcontainer", | ||||
|         "server-dev": "npm run start-server-dev", | ||||
|         "open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME" | ||||
|     }, | ||||
|     "customizations": { | ||||
|         "vscode": { | ||||
|             "extensions": [ | ||||
|                 "streetsidesoftware.code-spell-checker", | ||||
|                 "dbaeumer.vscode-eslint" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|     "forwardPorts": [3000, 3001] | ||||
| } | ||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/ask-for-help.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/ask-for-help.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -44,7 +44,7 @@ body: | ||||
|     id: operating-system | ||||
|     attributes: | ||||
|       label: "💻 Operating System and Arch" | ||||
|       description: "Which OS is your server/device running on?" | ||||
|       description: "Which OS is your server/device running on? (For Replit, please do not report this bug)" | ||||
|       placeholder: "Ex. Ubuntu 20.04 x86" | ||||
|     validations: | ||||
|       required: true | ||||
| @@ -52,7 +52,7 @@ body: | ||||
|     id: browser-vendor | ||||
|     attributes: | ||||
|       label: "🌐 Browser" | ||||
|       description: "Which browser are you running on?" | ||||
|       description: "Which browser are you running on? (For Replit, please do not report this bug)" | ||||
|       placeholder: "Ex. Google Chrome 95.0.4638.69" | ||||
|     validations: | ||||
|       required: true | ||||
|   | ||||
							
								
								
									
										9
									
								
								.github/workflows/auto-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.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 | ||||
| @@ -33,7 +33,6 @@ 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 | ||||
| @@ -51,7 +50,7 @@ jobs: | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [ ARMv7 ] | ||||
|         node: [ 14, 18 ] | ||||
|         node: [ 14.21.3, 18.16.1 ] | ||||
|         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ | ||||
|  | ||||
|     steps: | ||||
| @@ -62,7 +61,6 @@ jobs: | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node }} | ||||
|           cache: 'npm' | ||||
|       - run: npm install npm@latest -g | ||||
|       - run: npm ci --production | ||||
|  | ||||
| @@ -77,7 +75,6 @@ jobs: | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: 14 | ||||
|         cache: 'npm' | ||||
|     - run: npm install | ||||
|     - run: npm run lint | ||||
|  | ||||
| @@ -92,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 | ||||
| @@ -108,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 | ||||
|   | ||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @@ -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 | ||||
| @@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001 | ||||
|  | ||||
| ### 💪🏻 Non-Docker | ||||
|  | ||||
| Requirements:  | ||||
| Requirements: | ||||
| - Platform | ||||
|   - ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.  | ||||
|   - ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc. | ||||
|   - ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher | ||||
|   - ❌ Replit / Heroku | ||||
| - [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported) | ||||
| - [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4 | ||||
| - [npm](https://docs.npmjs.com/cli/) >= 7 | ||||
| - [Git](https://git-scm.com/downloads)  | ||||
| - [Git](https://git-scm.com/downloads) | ||||
| - [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background | ||||
|  | ||||
| ```bash | ||||
| @@ -71,7 +71,7 @@ npm run setup | ||||
| node server/server.js | ||||
|  | ||||
| # (Recommended) Option 2. Run in background using PM2 | ||||
| # Install PM2 if you don't have it:  | ||||
| # Install PM2 if you don't have it: | ||||
| npm install pm2 -g && pm2 install pm2-logrotate | ||||
|  | ||||
| # Start Server | ||||
|   | ||||
| @@ -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"); | ||||
| @@ -16,8 +17,12 @@ export default defineConfig({ | ||||
|     }, | ||||
|     define: { | ||||
|         "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), | ||||
|         "DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER), | ||||
|         "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN), | ||||
|         "CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME), | ||||
|     }, | ||||
|     plugins: [ | ||||
|         commonjs(), | ||||
|         vue(), | ||||
|         legacy({ | ||||
|             targets: [ "since 2015" ], | ||||
| @@ -42,6 +47,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; | ||||
							
								
								
									
										22
									
								
								db/patch-added-kafka-producer.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								db/patch-added-kafka-producer.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_topic VARCHAR(255); | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_brokers TEXT; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_ssl INTEGER; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_allow_auto_topic_creation VARCHAR(255); | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_sasl_options TEXT; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
| 	ADD kafka_producer_message TEXT; | ||||
|  | ||||
| COMMIT; | ||||
| @@ -2,13 +2,35 @@ | ||||
| FROM node:18-bullseye-slim AS base2-slim | ||||
| ARG TARGETPLATFORM | ||||
|  | ||||
| RUN apt update && \ | ||||
|     apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ | ||||
|         sqlite3 iputils-ping util-linux dumb-init git curl ca-certificates && \ | ||||
|     pip3 --no-cache-dir install apprise==1.4.0 && \ | ||||
| WORKDIR /app | ||||
|  | ||||
| # Specify --no-install-recommends to skip unused dependencies, make the base much smaller! | ||||
| # python3* = apprise's dependencies | ||||
| # sqlite3 = for debugging | ||||
| # iputils-ping = for ping | ||||
| # util-linux = for setpriv (Should be dropped in 2.0.0?) | ||||
| # dumb-init = avoid zombie processes (#480) | ||||
| # curl = for debugging | ||||
| # ca-certificates = keep the cert up-to-date | ||||
| # sudo = for start service nscd with non-root user | ||||
| # nscd = for better DNS caching | ||||
| # (pip) apprise = for notifications | ||||
| RUN apt-get update && \ | ||||
|     apt-get --yes --no-install-recommends install  \ | ||||
|         python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ | ||||
|         sqlite3  \ | ||||
|         iputils-ping  \ | ||||
|         util-linux  \ | ||||
|         dumb-init  \ | ||||
|         curl  \ | ||||
|         ca-certificates \ | ||||
|         sudo \ | ||||
|         nscd && \ | ||||
|     pip3 --no-cache-dir install apprise==1.4.5 && \ | ||||
|     rm -rf /var/lib/apt/lists/* && \ | ||||
|     apt --yes autoremove | ||||
|  | ||||
|  | ||||
| # Install cloudflared | ||||
| RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \ | ||||
|     echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | tee /etc/apt/sources.list.d/cloudflared.list && \ | ||||
| @@ -18,6 +40,11 @@ RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyr | ||||
|     rm -rf /var/lib/apt/lists/* && \ | ||||
|     apt --yes autoremove | ||||
|  | ||||
| # For nscd | ||||
| COPY ./docker/etc/nscd.conf /etc/nscd.conf | ||||
| COPY ./docker/etc/sudoers /etc/sudoers | ||||
|  | ||||
|  | ||||
| # Full Base Image | ||||
| # MariaDB, Chromium and fonts | ||||
| # Not working for armv7, so use the older version (10.5) of MariaDB from the debian repo | ||||
| @@ -30,5 +57,3 @@ RUN apt update && \ | ||||
|     rm -rf /var/lib/apt/lists/* && \ | ||||
|     apt --yes autoremove && \ | ||||
|     chown -R node:node /var/lib/mysql | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										90
									
								
								docker/etc/nscd.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								docker/etc/nscd.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| # | ||||
| # /etc/nscd.conf | ||||
| # | ||||
| # An example Name Service Cache config file.  This file is needed by nscd. | ||||
| # | ||||
| # Legal entries are: | ||||
| # | ||||
| #       logfile                 <file> | ||||
| #       debug-level             <level> | ||||
| #       threads                 <initial #threads to use> | ||||
| #       max-threads             <maximum #threads to use> | ||||
| #       server-user             <user to run server as instead of root> | ||||
| #               server-user is ignored if nscd is started with -S parameters | ||||
| #       stat-user               <user who is allowed to request statistics> | ||||
| #       reload-count            unlimited|<number> | ||||
| #       paranoia                <yes|no> | ||||
| #       restart-interval        <time in seconds> | ||||
| # | ||||
| #       enable-cache            <service> <yes|no> | ||||
| #       positive-time-to-live   <service> <time in seconds> | ||||
| #       negative-time-to-live   <service> <time in seconds> | ||||
| #       suggested-size          <service> <prime number> | ||||
| #       check-files             <service> <yes|no> | ||||
| #       persistent              <service> <yes|no> | ||||
| #       shared                  <service> <yes|no> | ||||
| #       max-db-size             <service> <number bytes> | ||||
| #       auto-propagate          <service> <yes|no> | ||||
| # | ||||
| # Currently supported cache names (services): passwd, group, hosts, services | ||||
| # | ||||
|  | ||||
|  | ||||
| #       logfile                 /var/log/nscd.log | ||||
| #       threads                 4 | ||||
| #       max-threads             32 | ||||
| #        server-user             node | ||||
| #       stat-user               somebody | ||||
|         debug-level             0 | ||||
| #       reload-count            5 | ||||
|         paranoia                no | ||||
| #       restart-interval        3600 | ||||
|  | ||||
|         enable-cache            passwd          no | ||||
|         positive-time-to-live   passwd          600 | ||||
|         negative-time-to-live   passwd          20 | ||||
|         suggested-size          passwd          211 | ||||
|         check-files             passwd          yes | ||||
|         persistent              passwd          yes | ||||
|         shared                  passwd          yes | ||||
|         max-db-size             passwd          33554432 | ||||
|         auto-propagate          passwd          yes | ||||
|  | ||||
|         enable-cache            group           no | ||||
|         positive-time-to-live   group           3600 | ||||
|         negative-time-to-live   group           60 | ||||
|         suggested-size          group           211 | ||||
|         check-files             group           yes | ||||
|         persistent              group           yes | ||||
|         shared                  group           yes | ||||
|         max-db-size             group           33554432 | ||||
|         auto-propagate          group           yes | ||||
|  | ||||
|         enable-cache            hosts           yes | ||||
|         positive-time-to-live   hosts           3600 | ||||
|         negative-time-to-live   hosts           20 | ||||
|         suggested-size          hosts           211 | ||||
|         check-files             hosts           yes | ||||
|         persistent              hosts           yes | ||||
| # Set shared to "no" to display stats in `nscd -g` | ||||
| # Read more: https://stackoverflow.com/questions/40429245/nscdcentos7curl-0-dns-cache-hit-rate | ||||
|         shared                  hosts           no | ||||
|         max-db-size             hosts           33554432 | ||||
|  | ||||
|         enable-cache            services        no | ||||
|         positive-time-to-live   services        28800 | ||||
|         negative-time-to-live   services        20 | ||||
|         suggested-size          services        211 | ||||
|         check-files             services        yes | ||||
|         persistent              services        yes | ||||
|         shared                  services        yes | ||||
|         max-db-size             services        33554432 | ||||
|  | ||||
|         enable-cache            netgroup        no | ||||
|         positive-time-to-live   netgroup        28800 | ||||
|         negative-time-to-live   netgroup        20 | ||||
|         suggested-size          netgroup        211 | ||||
|         check-files             netgroup        yes | ||||
|         persistent              netgroup        yes | ||||
|         shared                  netgroup        yes | ||||
|         max-db-size             netgroup        33554432 | ||||
							
								
								
									
										31
									
								
								docker/etc/sudoers
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								docker/etc/sudoers
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # | ||||
| # This file MUST be edited with the 'visudo' command as root. | ||||
| # | ||||
| # Please consider adding local content in /etc/sudoers.d/ instead of | ||||
| # directly modifying this file. | ||||
| # | ||||
| # See the man page for details on how to write a sudoers file. | ||||
| # | ||||
| Defaults        env_reset | ||||
| Defaults        mail_badpass | ||||
| Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" | ||||
|  | ||||
| # Host alias specification | ||||
|  | ||||
| # User alias specification | ||||
|  | ||||
| # Cmnd alias specification | ||||
|  | ||||
| # User privilege specification | ||||
| root    ALL=(ALL:ALL) ALL | ||||
|  | ||||
| # Allow members of group sudo to execute any command | ||||
| %sudo   ALL=(ALL:ALL) ALL | ||||
|  | ||||
| # See sudoers(5) for more information on "#include" directives: | ||||
|  | ||||
| #includedir /etc/sudoers.d | ||||
|  | ||||
| # Allow `node` to control service (mainly for nscd) | ||||
| node ALL=(root) NOPASSWD: /usr/sbin/nscdservice | ||||
| node ALL=(root) NOPASSWD: /usr/sbin/service | ||||
| @@ -5,15 +5,15 @@ | ||||
|  | ||||
| // curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh | ||||
| println("====================="); | ||||
| println("Uptime Kuma Installer"); | ||||
| println("Uptime Kuma Install Script"); | ||||
| println("====================="); | ||||
| println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"); | ||||
| println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"); | ||||
| println("---------------------------------------"); | ||||
| println("This script is designed for Linux and basic usage."); | ||||
| println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"); | ||||
| println("---------------------------------------"); | ||||
| println(""); | ||||
| println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"); | ||||
| println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"); | ||||
| println("Docker - Install Uptime Kuma Docker container"); | ||||
| println(""); | ||||
|  | ||||
| @@ -29,14 +29,10 @@ function checkNode() { | ||||
|     bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')"); | ||||
|     println("Node Version: " ++ nodeVersion); | ||||
|  | ||||
|     if (nodeVersion < "12") { | ||||
|     if (nodeVersion <= "12") { | ||||
|         println("Error: Required Node.js 14"); | ||||
|         call("exit", "1"); | ||||
|     } | ||||
|  | ||||
|     if (nodeVersion == "12") { | ||||
|         println("Warning: NodeJS " ++ nodeVersion ++ " is not tested."); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function deb() { | ||||
| @@ -60,8 +56,8 @@ function deb() { | ||||
|             bash("apt --yes install curl"); | ||||
|         } | ||||
|  | ||||
|         println("Installing Node.js 14"); | ||||
|         bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt"); | ||||
|         println("Installing Node.js 16"); | ||||
|         bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt"); | ||||
|         bash("apt --yes install nodejs"); | ||||
|         bash("node -v"); | ||||
|  | ||||
| @@ -91,6 +87,10 @@ if (type == "local") { | ||||
|         bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')"); | ||||
|         if (os == "Ubuntu") { | ||||
|             distribution = "ubuntu"; | ||||
|  | ||||
|             // Get ubuntu version | ||||
|             bash(". /etc/lsb-release"); | ||||
|             version = DISTRIB_RELEASE; | ||||
|         } | ||||
|         if (os == "Debian") { | ||||
|             distribution = "debian"; | ||||
| @@ -101,6 +101,7 @@ if (type == "local") { | ||||
|  | ||||
|     println("Your OS: " ++ os); | ||||
|     println("Distribution: " ++ distribution); | ||||
|     println("Version: " ++ version); | ||||
|     println("Arch: " ++ arch); | ||||
|  | ||||
|     if ("$3" != "") { | ||||
| @@ -131,15 +132,32 @@ if (type == "local") { | ||||
|             checkNode(); | ||||
|         } else { | ||||
|  | ||||
|             bash("curlCheck=$(curl --version)"); | ||||
|             if (curlCheck == "") { | ||||
|                 println("Installing Curl"); | ||||
|                 bash("yum -y -q install curl"); | ||||
|             bash("dnfCheck=$(dnf --version)"); | ||||
|  | ||||
|             // Use yum | ||||
|             if (dnfCheck == "") { | ||||
|                 bash("curlCheck=$(curl --version)"); | ||||
|                 if (curlCheck == "") { | ||||
|                     println("Installing Curl"); | ||||
|                     bash("yum -y -q install curl"); | ||||
|                 } | ||||
|  | ||||
|                 println("Installing Node.js 16"); | ||||
|                 bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt"); | ||||
|                 bash("yum install -y -q nodejs"); | ||||
|             } else { | ||||
|                 bash("curlCheck=$(curl --version)"); | ||||
|                 if (curlCheck == "") { | ||||
|                     println("Installing Curl"); | ||||
|                     bash("dnf -y install curl"); | ||||
|                 } | ||||
|  | ||||
|                 println("Installing Node.js 16"); | ||||
|                 bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt"); | ||||
|                 bash("dnf install -y nodejs"); | ||||
|             } | ||||
|  | ||||
|             println("Installing Node.js 14"); | ||||
|             bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt"); | ||||
|             bash("yum install -y -q nodejs"); | ||||
|  | ||||
|             bash("node -v"); | ||||
|  | ||||
|             bash("nodeCheckAgain=$(node -v)"); | ||||
| @@ -193,6 +211,14 @@ if (type == "local") { | ||||
|        bash("pm2 startup"); | ||||
|    } | ||||
|  | ||||
|  | ||||
|    // Check again | ||||
|    bash("check=$(pm2 --version)"); | ||||
|    if (check == "") { | ||||
|        println("Error: pm2 is not found!"); | ||||
|        bash("exit 1"); | ||||
|    } | ||||
|  | ||||
|    bash("mkdir -p $installPath"); | ||||
|    bash("cd $installPath"); | ||||
|    bash("git clone https://github.com/louislam/uptime-kuma.git ."); | ||||
|   | ||||
							
								
								
									
										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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										54
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								install.sh
									
									
									
									
									
								
							| @@ -3,15 +3,15 @@ | ||||
| # The command is working on Windows PowerShell and Docker for Windows only. | ||||
| # curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh | ||||
| "echo" "-e" "=====================" | ||||
| "echo" "-e" "Uptime Kuma Installer" | ||||
| "echo" "-e" "Uptime Kuma Install Script" | ||||
| "echo" "-e" "=====================" | ||||
| "echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian" | ||||
| "echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8" | ||||
| "echo" "-e" "---------------------------------------" | ||||
| "echo" "-e" "This script is designed for Linux and basic usage." | ||||
| "echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation" | ||||
| "echo" "-e" "---------------------------------------" | ||||
| "echo" "-e" "" | ||||
| "echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2" | ||||
| "echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2" | ||||
| "echo" "-e" "Docker - Install Uptime Kuma Docker container" | ||||
| "echo" "-e" "" | ||||
| if [ "$1" != "" ]; then | ||||
| @@ -25,12 +25,9 @@ function checkNode { | ||||
|   nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])') | ||||
|   "echo" "-e" "Node Version: ""$nodeVersion" | ||||
|   _0="12" | ||||
|   if [ $(($nodeVersion < $_0)) == 1 ]; then | ||||
|   if [ $(($nodeVersion <= $_0)) == 1 ]; then | ||||
|     "echo" "-e" "Error: Required Node.js 14" | ||||
|     "exit" "1"   | ||||
| fi | ||||
|   if [ "$nodeVersion" == "12" ]; then | ||||
|     "echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."   | ||||
| fi | ||||
| } | ||||
| function deb { | ||||
| @@ -50,8 +47,8 @@ fi | ||||
|       "echo" "-e" "Installing Curl" | ||||
|       apt --yes install curl     | ||||
| fi | ||||
|     "echo" "-e" "Installing Node.js 14" | ||||
|     curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt | ||||
|     "echo" "-e" "Installing Node.js 16" | ||||
|     curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt | ||||
|     apt --yes install nodejs | ||||
|     node -v | ||||
|     nodeCheckAgain=$(node -v) | ||||
| @@ -75,7 +72,10 @@ if [ "$type" == "local" ]; then | ||||
|     if [ -e "/etc/issue" ]; then | ||||
|       os=$(head -n1 /etc/issue | cut -f 1 -d ' ') | ||||
|       if [ "$os" == "Ubuntu" ]; then | ||||
|         distribution="ubuntu"       | ||||
|         distribution="ubuntu" | ||||
|         # Get ubuntu version | ||||
|         . /etc/lsb-release | ||||
|         version="$DISTRIB_RELEASE"       | ||||
| fi | ||||
|       if [ "$os" == "Debian" ]; then | ||||
|         distribution="debian"       | ||||
| @@ -85,6 +85,7 @@ fi | ||||
|   arch=$(uname -i) | ||||
|   "echo" "-e" "Your OS: ""$os" | ||||
|   "echo" "-e" "Distribution: ""$distribution" | ||||
|   "echo" "-e" "Version: ""$version" | ||||
|   "echo" "-e" "Arch: ""$arch" | ||||
|   if [ "$3" != "" ]; then | ||||
|     port="$3" | ||||
| @@ -108,14 +109,27 @@ fi | ||||
|     if [ "$nodeCheck" != "" ]; then | ||||
|       "checkNode"  | ||||
|     else | ||||
|       curlCheck=$(curl --version) | ||||
|       if [ "$curlCheck" == "" ]; then | ||||
|         "echo" "-e" "Installing Curl" | ||||
|         yum -y -q install curl       | ||||
|       dnfCheck=$(dnf --version) | ||||
|       # Use yum | ||||
|       if [ "$dnfCheck" == "" ]; then | ||||
|         curlCheck=$(curl --version) | ||||
|         if [ "$curlCheck" == "" ]; then | ||||
|           "echo" "-e" "Installing Curl" | ||||
|           yum -y -q install curl         | ||||
| fi | ||||
|       "echo" "-e" "Installing Node.js 14" | ||||
|       curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt | ||||
|       yum install -y -q nodejs | ||||
|         "echo" "-e" "Installing Node.js 16" | ||||
|         curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt | ||||
|         yum install -y -q nodejs | ||||
|       else | ||||
|         curlCheck=$(curl --version) | ||||
|         if [ "$curlCheck" == "" ]; then | ||||
|           "echo" "-e" "Installing Curl" | ||||
|           dnf -y install curl         | ||||
| fi | ||||
|         "echo" "-e" "Installing Node.js 16" | ||||
|         curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt | ||||
|         dnf install -y nodejs | ||||
|       fi | ||||
|       node -v | ||||
|       nodeCheckAgain=$(node -v) | ||||
|       if [ "$nodeCheckAgain" == "" ]; then | ||||
| @@ -161,6 +175,12 @@ fi | ||||
|     "echo" "-e" "Installing PM2" | ||||
|     npm install pm2 -g && pm2 install pm2-logrotate | ||||
|     pm2 startup   | ||||
| fi | ||||
|   # Check again | ||||
|   check=$(pm2 --version) | ||||
|   if [ "$check" == "" ]; then | ||||
|     "echo" "-e" "Error: pm2 is not found!" | ||||
|     exit 1   | ||||
| fi | ||||
|   mkdir -p $installPath | ||||
|   cd $installPath | ||||
|   | ||||
							
								
								
									
										46
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
| { | ||||
|     "name": "uptime-kuma", | ||||
|     "version": "1.22.0", | ||||
|     "version": "1.22.1", | ||||
|     "license": "MIT", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|         "url": "https://github.com/louislam/uptime-kuma.git" | ||||
|     }, | ||||
|     "engines": { | ||||
|         "node": "14.* || 16.* || 18.*" | ||||
|         "node": "14 || 16 || 18 || >= 20.4.0" | ||||
|     }, | ||||
|     "scripts": { | ||||
|         "install-legacy": "npm install", | ||||
| @@ -19,6 +19,7 @@ | ||||
|         "lint": "npm run lint:js && npm run lint:style", | ||||
|         "dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"", | ||||
|         "start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js", | ||||
|         "start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js", | ||||
|         "start": "npm run start-server", | ||||
|         "start-server": "node server/server.js", | ||||
|         "start-server-dev": "cross-env NODE_ENV=development node server/server.js", | ||||
| @@ -34,24 +35,28 @@ | ||||
|         "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-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push", | ||||
|         "build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --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:nightly2 --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:nightly2 --target nightly . --push", | ||||
|         "build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .", | ||||
|         "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.22.0 && 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", | ||||
|         "remove-2fa": "node extra/remove-2fa.js", | ||||
|         "compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1", | ||||
|         "test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .", | ||||
|         "test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .", | ||||
|         "test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .", | ||||
|         "test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .", | ||||
|         "test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .", | ||||
|         "test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .", | ||||
|         "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", | ||||
|         "simple-dns-server": "node extra/simple-dns-server.js", | ||||
|         "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", | ||||
|         "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", | ||||
| @@ -67,7 +72,7 @@ | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@grpc/grpc-js": "~1.7.3", | ||||
|         "@louislam/ping": "~0.4.4-mod.0", | ||||
|         "@louislam/ping": "~0.4.4-mod.1", | ||||
|         "@louislam/sqlite3": "15.1.6", | ||||
|         "args-parser": "~1.3.0", | ||||
|         "axios": "~0.27.0", | ||||
| @@ -95,10 +100,13 @@ | ||||
|         "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", | ||||
|         "kafkajs": "^2.2.4", | ||||
|         "knex": "^2.4.2", | ||||
|         "limiter": "~2.1.0", | ||||
|         "liquidjs": "^10.7.0", | ||||
|         "mongodb": "~4.14.0", | ||||
|         "mqtt": "~4.3.7", | ||||
|         "mssql": "~8.1.4", | ||||
| @@ -114,10 +122,11 @@ | ||||
|         "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.3.0", | ||||
|         "redis": "~4.5.1", | ||||
|         "semver": "~7.5.4", | ||||
|         "socket.io": "~4.6.1", | ||||
|         "socket.io-client": "~4.6.1", | ||||
|         "socks-proxy-agent": "6.1.1", | ||||
| @@ -127,7 +136,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 +144,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 +157,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 +174,16 @@ | ||||
|         "qrcode": "~1.5.0", | ||||
|         "rollup-plugin-visualizer": "^5.6.0", | ||||
|         "sass": "~1.42.1", | ||||
|         "stylelint": "~15.9.0", | ||||
|         "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", | ||||
|   | ||||
| @@ -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"); | ||||
| const path = require("path"); | ||||
| const { EmbeddedMariaDB } = require("./embedded-mariadb"); | ||||
| const mysql = require("mysql2/promise"); | ||||
| @@ -77,6 +76,9 @@ class Database { | ||||
|         "patch-monitor-tls.sql": true, | ||||
|         "patch-maintenance-cron.sql": true, | ||||
|         "patch-add-parent-monitor.sql": true,   // The last file so far converted to a knex migration file | ||||
|         "patch-add-invert-keyword.sql": true, | ||||
|         "patch-added-json-query.sql": true, | ||||
|         "patch-added-kafka-producer.sql": true, | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
| @@ -99,12 +101,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.sqlitePath = Database.dataDir + "kuma.db"; | ||||
|         if (! fs.existsSync(Database.dataDir)) { | ||||
|             fs.mkdirSync(Database.dataDir, { recursive: true }); | ||||
|   | ||||
| @@ -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, | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -42,6 +42,8 @@ const clearOldData = async () => { | ||||
|                 "DELETE FROM heartbeat WHERE time < " + sqlHourOffset, | ||||
|                 [ parsedPeriod * -24 ] | ||||
|             ); | ||||
|  | ||||
|             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, | ||||
| }; | ||||
| @@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA | ||||
|     SQL_DATETIME_FORMAT | ||||
| } = require("../../src/util"); | ||||
| const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery, | ||||
|     redisPingAsync, mongodbPing, | ||||
|     redisPingAsync, mongodbPing, kafkaProducerAsync | ||||
| } = require("../util-server"); | ||||
| const { R } = require("redbean-node"); | ||||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
| @@ -20,6 +20,7 @@ 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"); | ||||
| const Database = require("../database"); | ||||
|  | ||||
| @@ -98,6 +99,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(), | ||||
| @@ -126,6 +128,13 @@ class Monitor extends BeanModel { | ||||
|             radiusCallingStationId: this.radiusCallingStationId, | ||||
|             game: this.game, | ||||
|             httpBodyEncoding: this.httpBodyEncoding, | ||||
|             jsonPath: this.jsonPath, | ||||
|             expectedValue: this.expectedValue, | ||||
|             kafkaProducerTopic: this.kafkaProducerTopic, | ||||
|             kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), | ||||
|             kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false, | ||||
|             kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false, | ||||
|             kafkaProducerMessage: this.kafkaProducerMessage, | ||||
|             screenshot, | ||||
|         }; | ||||
|  | ||||
| @@ -150,6 +159,7 @@ class Monitor extends BeanModel { | ||||
|                 tlsCa: this.tlsCa, | ||||
|                 tlsCert: this.tlsCert, | ||||
|                 tlsKey: this.tlsKey, | ||||
|                 kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -164,7 +174,7 @@ class Monitor extends BeanModel { | ||||
|     async isActive() { | ||||
|         const parentActive = await Monitor.isParentActive(this.id); | ||||
|  | ||||
|         return this.active && parentActive; | ||||
|         return (this.active === 1) && parentActive; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -208,6 +218,14 @@ class Monitor extends BeanModel { | ||||
|         return Boolean(this.upsideDown); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse to boolean | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     isInvertKeyword() { | ||||
|         return Boolean(this.invertKeyword); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse to boolean | ||||
|      * @returns {boolean} | ||||
| @@ -312,7 +330,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(); | ||||
|  | ||||
| @@ -440,7 +458,7 @@ class Monitor extends BeanModel { | ||||
|  | ||||
|                     if (this.type === "http") { | ||||
|                         bean.status = UP; | ||||
|                     } else { | ||||
|                     } else if (this.type === "keyword") { | ||||
|  | ||||
|                         let data = res.data; | ||||
|  | ||||
| @@ -449,17 +467,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") { | ||||
| @@ -534,7 +572,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 { | ||||
| @@ -627,9 +665,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); | ||||
|                     } | ||||
| @@ -658,7 +702,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; | ||||
| @@ -671,13 +714,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") { | ||||
| @@ -724,7 +768,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; | ||||
| @@ -754,6 +799,24 @@ class Monitor extends BeanModel { | ||||
|                         bean.ping = dayjs().valueOf() - startTime; | ||||
|                     } | ||||
|  | ||||
|                 } else if (this.type === "kafka-producer") { | ||||
|                     let startTime = dayjs().valueOf(); | ||||
|  | ||||
|                     bean.msg = await kafkaProducerAsync( | ||||
|                         JSON.parse(this.kafkaProducerBrokers), | ||||
|                         this.kafkaProducerTopic, | ||||
|                         this.kafkaProducerMessage, | ||||
|                         { | ||||
|                             allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation, | ||||
|                             ssl: this.kafkaProducerSsl, | ||||
|                             clientId: `Uptime-Kuma/${version}`, | ||||
|                             interval: this.interval, | ||||
|                         }, | ||||
|                         JSON.parse(this.kafkaProducerSaslOptions), | ||||
|                     ); | ||||
|                     bean.status = UP; | ||||
|                     bean.ping = dayjs().valueOf() - startTime; | ||||
|  | ||||
|                 } else { | ||||
|                     throw new Error("Unknown Monitor Type"); | ||||
|                 } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const { MonitorType } = require("./monitor-type"); | ||||
| const { chromium, Browser } = require("playwright-core"); | ||||
| const { chromium } = require("playwright-core"); | ||||
| const { UP, log } = require("../../src/util"); | ||||
| const { Settings } = require("../settings"); | ||||
| const commandExistsSync = require("command-exists").sync; | ||||
| @@ -7,13 +7,60 @@ const childProcess = require("child_process"); | ||||
| const path = require("path"); | ||||
| const Database = require("../database"); | ||||
| const jwt = require("jsonwebtoken"); | ||||
| const config = require("../config"); | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @type {Browser} | ||||
|  */ | ||||
| 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"); | ||||
| @@ -31,6 +78,7 @@ async function getBrowser() { | ||||
| 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) { | ||||
| @@ -60,30 +108,30 @@ async function prepareChromeExecutable(executablePath) { | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|         } else if (process.platform === "win32") { | ||||
|             executablePath = findChrome([ | ||||
|                 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "D:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "D:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "E:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", | ||||
|                 "E:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", | ||||
|             ]); | ||||
|         } else if (process.platform === "linux") { | ||||
|             executablePath = findChrome([ | ||||
|                 "chromium-browser", | ||||
|                 "chromium", | ||||
|                 "google-chrome", | ||||
|             ]); | ||||
|         } 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."); | ||||
|         } | ||||
|         // TODO: Mac?? | ||||
|     } | ||||
|     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; | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										95
									
								
								server/monitor-types/tailscale-ping.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								server/monitor-types/tailscale-ping.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| const { MonitorType } = require("./monitor-type"); | ||||
| const { UP, log } = require("../../src/util"); | ||||
| const exec = require("child_process").exec; | ||||
|  | ||||
| /** | ||||
|  * A TailscalePing class extends the MonitorType. | ||||
|  * It runs Tailscale ping to monitor the status of a specific node. | ||||
|  */ | ||||
| class TailscalePing extends MonitorType { | ||||
|  | ||||
|     name = "tailscale-ping"; | ||||
|  | ||||
|     /** | ||||
|      * Checks the ping status of the URL associated with the monitor. | ||||
|      * It then parses the Tailscale ping command output to update the heatrbeat. | ||||
|      * | ||||
|      * @param {Object} monitor - The monitor object associated with the check. | ||||
|      * @param {Object} heartbeat - The heartbeat object to update. | ||||
|      * @throws Will throw an error if checking Tailscale ping encounters any error | ||||
|      */ | ||||
|     async check(monitor, heartbeat) { | ||||
|         try { | ||||
|             let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval); | ||||
|             this.parseTailscaleOutput(tailscaleOutput, heartbeat); | ||||
|         } catch (err) { | ||||
|             log.debug("Tailscale", err); | ||||
|             // trigger log function somewhere to display a notification or alert to the user (but how?) | ||||
|             throw new Error(`Error checking Tailscale ping: ${err}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Runs the Tailscale ping command to the given URL. | ||||
|      * | ||||
|      * @param {string} hostname - The hostname to ping. | ||||
|      * @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command | ||||
|      * @throws Will throw an error if the command execution encounters any error. | ||||
|      */ | ||||
|     async runTailscalePing(hostname, interval) { | ||||
|         let cmd = `tailscale ping ${hostname}`; | ||||
|  | ||||
|         log.debug("Tailscale", cmd); | ||||
|  | ||||
|         return new Promise((resolve, reject) => { | ||||
|             let timeout = interval * 1000 * 0.8; | ||||
|             exec(cmd, { timeout: timeout }, (error, stdout, stderr) => { | ||||
|                 // we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues) | ||||
|                 if (error) { | ||||
|                     reject(`Execution error: ${error.message}`); | ||||
|                     return; | ||||
|                 } | ||||
|                 if (stderr) { | ||||
|                     reject(`Error in output: ${stderr}`); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 resolve(stdout); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parses the output of the Tailscale ping command to update the heartbeat. | ||||
|      * | ||||
|      * @param {string} tailscaleOutput - The output of the Tailscale ping command. | ||||
|      * @param {Object} heartbeat - The heartbeat object to update. | ||||
|      * @throws Will throw an eror if the output contains any unexpected string. | ||||
|      */ | ||||
|     parseTailscaleOutput(tailscaleOutput, heartbeat) { | ||||
|         let lines = tailscaleOutput.split("\n"); | ||||
|  | ||||
|         for (let line of lines) { | ||||
|             if (line.includes("pong from")) { | ||||
|                 heartbeat.status = UP; | ||||
|                 let time = line.split(" in ")[1].split(" ")[0]; | ||||
|                 heartbeat.ping = parseInt(time); | ||||
|                 heartbeat.msg = line; | ||||
|                 break; | ||||
|             } else if (line.includes("timed out")) { | ||||
|                 throw new Error(`Ping timed out: "${line}"`); | ||||
|                 // Immediately throws upon "timed out" message, the server is expected to re-call the check function | ||||
|             } else if (line.includes("no matching peer")) { | ||||
|                 throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`); | ||||
|             } else if (line.includes("is local Tailscale IP")) { | ||||
|                 throw new Error(`Tailscale only works if used on other machines: "${line}"`); | ||||
|             } else if (line !== "") { | ||||
|                 throw new Error(`Unexpected output: "${line}"`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     TailscalePing, | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -13,7 +13,7 @@ class SMTP extends NotificationProvider { | ||||
|             port: notification.smtpPort, | ||||
|             secure: notification.smtpSecure, | ||||
|             tls: { | ||||
|                 rejectUnauthorized: notification.smtpIgnoreTLSError || false, | ||||
|                 rejectUnauthorized: !notification.smtpIgnoreTLSError || false, | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -447,7 +447,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; | ||||
|   | ||||
| @@ -15,18 +15,25 @@ dayjs.extend(require("dayjs/plugin/customParseFormat")); | ||||
| require("dotenv").config(); | ||||
|  | ||||
| // Check Node.js Version | ||||
| const nodeVersion = parseInt(process.versions.node.split(".")[0]); | ||||
| const requiredVersion = 14; | ||||
| const nodeVersion = process.versions.node; | ||||
|  | ||||
| // Get the required Node.js version from package.json | ||||
| const requiredNodeVersions = require("../package.json").engines.node; | ||||
| const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* "; | ||||
| console.log(`Your Node.js version: ${nodeVersion}`); | ||||
|  | ||||
| // See more: https://github.com/louislam/uptime-kuma/issues/3138 | ||||
| if (nodeVersion >= 20) { | ||||
|     console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18."); | ||||
| const semver = require("semver"); | ||||
| const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", "); | ||||
|  | ||||
| // Exit Uptime Kuma immediately if the Node.js version is banned | ||||
| if (semver.satisfies(nodeVersion, bannedNodeVersions)) { | ||||
|     console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`); | ||||
|     process.exit(-1); | ||||
| } | ||||
|  | ||||
| if (nodeVersion < requiredVersion) { | ||||
|     console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`); | ||||
|     process.exit(-1); | ||||
| // Warning if the Node.js version is not in the support list, but it maybe still works | ||||
| if (!semver.satisfies(nodeVersion, requiredNodeVersions)) { | ||||
|     console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`); | ||||
| } | ||||
|  | ||||
| const args = require("args-parser")(process.argv); | ||||
| @@ -42,6 +49,7 @@ if (! process.env.NODE_ENV) { | ||||
| } | ||||
|  | ||||
| log.info("server", "Node Env: " + process.env.NODE_ENV); | ||||
| log.info("server", "Inside Container: " + process.env.UPTIME_KUMA_IS_CONTAINER === "1"); | ||||
|  | ||||
| log.info("server", "Importing Node libraries"); | ||||
| const fs = require("fs"); | ||||
| @@ -149,7 +157,6 @@ 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"); | ||||
| const { EmbeddedMariaDB } = require("./embedded-mariadb"); | ||||
| @@ -193,7 +200,6 @@ let needSetup = false; | ||||
|  | ||||
|     // Database should be ready now | ||||
|     await server.initAfterDatabaseReady(); | ||||
|     server.loadPlugins(); | ||||
|     server.entryPage = await Settings.get("entryPage"); | ||||
|     await StatusPage.loadDomainMappingList(); | ||||
|  | ||||
| @@ -239,6 +245,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); | ||||
| @@ -293,7 +300,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"); | ||||
| @@ -666,6 +673,9 @@ let needSetup = false; | ||||
|                 monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); | ||||
|                 delete monitor.accepted_statuscodes; | ||||
|  | ||||
|                 monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers); | ||||
|                 monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions); | ||||
|  | ||||
|                 bean.import(monitor); | ||||
|                 bean.user_id = socket.userID; | ||||
|  | ||||
| @@ -748,6 +758,7 @@ let needSetup = false; | ||||
|                 } | ||||
|  | ||||
|                 bean.keyword = monitor.keyword; | ||||
|                 bean.invertKeyword = monitor.invertKeyword; | ||||
|                 bean.ignoreTls = monitor.ignoreTls; | ||||
|                 bean.expiryNotification = monitor.expiryNotification; | ||||
|                 bean.upsideDown = monitor.upsideDown; | ||||
| @@ -782,6 +793,13 @@ let needSetup = false; | ||||
|                 bean.radiusCallingStationId = monitor.radiusCallingStationId; | ||||
|                 bean.radiusSecret = monitor.radiusSecret; | ||||
|                 bean.httpBodyEncoding = monitor.httpBodyEncoding; | ||||
|                 bean.expectedValue = monitor.expectedValue; | ||||
|                 bean.jsonPath = monitor.jsonPath; | ||||
|                 bean.kafkaProducerTopic = monitor.kafkaProducerTopic; | ||||
|                 bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers); | ||||
|                 bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation; | ||||
|                 bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions); | ||||
|                 bean.kafkaProducerMessage = monitor.kafkaProducerMessage; | ||||
|  | ||||
|                 bean.validate(); | ||||
|  | ||||
| @@ -1415,6 +1433,7 @@ 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, | ||||
| @@ -1583,7 +1602,6 @@ let needSetup = false; | ||||
|         maintenanceSocketHandler(socket); | ||||
|         apiKeySocketHandler(socket); | ||||
|         generalSocketHandler(socket, server); | ||||
|         pluginsHandler(socket, server); | ||||
|  | ||||
|         log.debug("server", "added all socket handlers"); | ||||
|  | ||||
| @@ -1609,6 +1627,8 @@ let needSetup = false; | ||||
|         await shutdownFunction(); | ||||
|     }); | ||||
|  | ||||
|     server.start(); | ||||
|  | ||||
|     server.httpServer.listen(port, hostname, () => { | ||||
|         if (hostname) { | ||||
|             log.info("server", `Listening on ${hostname}:${port}`); | ||||
| @@ -1686,6 +1706,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); | ||||
|   | ||||
| @@ -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,8 @@ 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()` | ||||
| const childProcess = require("child_process"); | ||||
| // 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 +47,6 @@ class UptimeKumaServer { | ||||
|      */ | ||||
|     indexHTML = ""; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Manager | ||||
|      * @type {PluginsManager} | ||||
|      */ | ||||
|     pluginsManager = null; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @type {{}} | ||||
| @@ -106,6 +100,7 @@ class UptimeKumaServer { | ||||
|  | ||||
|         // Set Monitor Types | ||||
|         UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); | ||||
|         UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); | ||||
|  | ||||
|         this.io = new Server(this.httpServer); | ||||
|     } | ||||
| @@ -256,9 +251,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:/, ""); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -269,13 +264,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"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -287,66 +312,79 @@ 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); | ||||
|     } | ||||
|  | ||||
|     /** Stop the server */ | ||||
|     /** | ||||
|      * TODO: Listen logic should be moved to here | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     async start() { | ||||
|         this.startServices(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Stop the server | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     async stop() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     loadPlugins() { | ||||
|         this.pluginsManager = new PluginsManager(this); | ||||
|         this.stopServices(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @returns {PluginsManager} | ||||
|      * Start all system services (e.g. nscd) | ||||
|      * For now, only used in Docker | ||||
|      */ | ||||
|     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"); | ||||
|     startServices() { | ||||
|         if (process.env.UPTIME_KUMA_IS_CONTAINER) { | ||||
|             try { | ||||
|                 log.info("services", "Starting nscd"); | ||||
|                 childProcess.execSync("sudo service nscd start", { stdio: "pipe" }); | ||||
|             } catch (e) { | ||||
|                 log.info("services", "Failed to start nscd"); | ||||
|             } | ||||
|             UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType; | ||||
|         } else { | ||||
|             log.error("", "Invalid Monitor Type: " + monitorType.name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {MonitorType} monitorType | ||||
|      * Stop all system services | ||||
|      */ | ||||
|     removeMonitorType(monitorType) { | ||||
|         if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) { | ||||
|             delete UptimeKumaServer.monitorTypeList[monitorType.name]; | ||||
|         } else { | ||||
|             log.error("", "Remove MonitorType failed: " + monitorType.name); | ||||
|     stopServices() { | ||||
|         if (process.env.UPTIME_KUMA_IS_CONTAINER) { | ||||
|             try { | ||||
|                 log.info("services", "Stopping nscd"); | ||||
|                 childProcess.execSync("sudo service nscd stop"); | ||||
|             } catch (e) { | ||||
|                 log.info("services", "Failed to stop nscd"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| 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"); | ||||
| const { TailscalePing } = require("./monitor-types/tailscale-ping"); | ||||
|   | ||||
| @@ -31,8 +31,11 @@ const readline = require("readline"); | ||||
| const rl = readline.createInterface({ input: process.stdin, | ||||
|     output: process.stdout }); | ||||
|  | ||||
| const isWindows = process.platform === /^win/.test(process.platform); | ||||
| // SASLOptions used in JSDoc | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { Kafka, SASLOptions } = require("kafkajs"); | ||||
|  | ||||
| const isWindows = process.platform === /^win/.test(process.platform); | ||||
| /** | ||||
|  * Init or reset JWT secret | ||||
|  * @returns {Promise<Bean>} | ||||
| @@ -199,6 +202,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) { | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Monitor Kafka using Producer | ||||
|  * @param {string} topic Topic name to produce into | ||||
|  * @param {string} message Message to produce | ||||
|  * @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}] | ||||
|  * Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and | ||||
|  * interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma" | ||||
|  * and ssl defaults to false) | ||||
|  * @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':' | ||||
|  * @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to | ||||
|  * {}) | ||||
|  * @returns {Promise<string>} | ||||
|  */ | ||||
| exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options; | ||||
|  | ||||
|         let connectedToKafka = false; | ||||
|  | ||||
|         const timeoutID = setTimeout(() => { | ||||
|             log.debug("kafkaProducer", "KafkaProducer timeout triggered"); | ||||
|             connectedToKafka = true; | ||||
|             reject(new Error("Timeout")); | ||||
|         }, interval * 1000 * 0.8); | ||||
|  | ||||
|         if (saslOptions.mechanism === "None") { | ||||
|             saslOptions = undefined; | ||||
|         } | ||||
|  | ||||
|         let client = new Kafka({ | ||||
|             brokers: brokers, | ||||
|             clientId: clientId, | ||||
|             sasl: saslOptions, | ||||
|             retry: { | ||||
|                 retries: 0, | ||||
|             }, | ||||
|             ssl: ssl, | ||||
|         }); | ||||
|  | ||||
|         let producer = client.producer({ | ||||
|             allowAutoTopicCreation: allowAutoTopicCreation, | ||||
|             retry: { | ||||
|                 retries: 0, | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         producer.connect().then( | ||||
|             () => { | ||||
|                 try { | ||||
|                     producer.send({ | ||||
|                         topic: topic, | ||||
|                         messages: [{ | ||||
|                             value: message, | ||||
|                         }], | ||||
|                     }); | ||||
|                     connectedToKafka = true; | ||||
|                     clearTimeout(timeoutID); | ||||
|                     resolve("Message sent successfully"); | ||||
|                 } catch (e) { | ||||
|                     connectedToKafka = true; | ||||
|                     producer.disconnect(); | ||||
|                     clearTimeout(timeoutID); | ||||
|                     reject(new Error("Error sending message: " + e.message)); | ||||
|                 } | ||||
|             } | ||||
|         ).catch( | ||||
|             (e) => { | ||||
|                 connectedToKafka = true; | ||||
|                 producer.disconnect(); | ||||
|                 clearTimeout(timeoutID); | ||||
|                 reject(new Error("Error in producer connection: " + e.message)); | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|         producer.on("producer.network.request_timeout", (_) => { | ||||
|             clearTimeout(timeoutID); | ||||
|             reject(new Error("producer.network.request_timeout")); | ||||
|         }); | ||||
|  | ||||
|         producer.on("producer.disconnect", (_) => { | ||||
|             if (!connectedToKafka) { | ||||
|                 clearTimeout(timeoutID); | ||||
|                 reject(new Error("producer.disconnect")); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Use NTLM Auth for a http request. | ||||
|  * @param {Object} options The http request options | ||||
| @@ -381,6 +472,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 ( | ||||
| @@ -391,10 +483,12 @@ exports.radius = function ( | ||||
|     callingStationId, | ||||
|     secret, | ||||
|     port = 1812, | ||||
|     timeout = 2500, | ||||
| ) { | ||||
|     const client = new radiusClient({ | ||||
|         host: hostname, | ||||
|         hostPort: port, | ||||
|         timeout: timeout, | ||||
|         dictionaries: [ file ], | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -436,12 +436,12 @@ optgroup { | ||||
| .monitor-list { | ||||
|     &.scrollbar { | ||||
|         overflow-y: auto; | ||||
|         height: calc(100% - 65px); | ||||
|         height: calc(100% - 107px); | ||||
|     } | ||||
|  | ||||
|     @media (max-width: 770px) { | ||||
|         &.scrollbar { | ||||
|             height: calc(100% - 40px); | ||||
|             height: calc(100% - 97px); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,25 @@ | ||||
| <template> | ||||
|     <div class="shadow-box mb-3" :style="boxStyle"> | ||||
|         <div class="list-header"> | ||||
|             <div class="placeholder"></div> | ||||
|             <div class="search-wrapper"> | ||||
|                 <a v-if="searchText == ''" class="search-icon"> | ||||
|                     <font-awesome-icon icon="search" /> | ||||
|                 </a> | ||||
|                 <a v-if="searchText != ''" class="search-icon" @click="clearSearchText"> | ||||
|                     <font-awesome-icon icon="times" /> | ||||
|                 </a> | ||||
|                 <form> | ||||
|                     <input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" /> | ||||
|                 </form> | ||||
|             <div class="header-top"> | ||||
|                 <div class="placeholder"></div> | ||||
|                 <div class="search-wrapper"> | ||||
|                     <a v-if="searchText == ''" class="search-icon"> | ||||
|                         <font-awesome-icon icon="search" /> | ||||
|                     </a> | ||||
|                     <a v-if="searchText != ''" class="search-icon" @click="clearSearchText"> | ||||
|                         <font-awesome-icon icon="times" /> | ||||
|                     </a> | ||||
|                     <form> | ||||
|                         <input | ||||
|                             v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" | ||||
|                             autocomplete="off" | ||||
|                         /> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="header-filter"> | ||||
|                 <MonitorListFilter :filterState="filterState" @update-filter="updateFilter" /> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="monitor-list" :class="{ scrollbar: scrollbar }"> | ||||
| @@ -19,18 +27,23 @@ | ||||
|                 {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> | ||||
|             </div> | ||||
|  | ||||
|             <MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" /> | ||||
|             <MonitorListItem | ||||
|                 v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" | ||||
|                 :isSearch="searchText !== ''" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import MonitorListItem from "../components/MonitorListItem.vue"; | ||||
| import MonitorListFilter from "./MonitorListFilter.vue"; | ||||
| import { getMonitorRelativeURL } from "../util.ts"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         MonitorListItem, | ||||
|         MonitorListFilter, | ||||
|     }, | ||||
|     props: { | ||||
|         /** Should the scrollbar be shown */ | ||||
| @@ -42,6 +55,11 @@ export default { | ||||
|         return { | ||||
|             searchText: "", | ||||
|             windowTop: 0, | ||||
|             filterState: { | ||||
|                 status: null, | ||||
|                 active: null, | ||||
|                 tags: null, | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -72,8 +90,8 @@ export default { | ||||
|                 const loweredSearchText = this.searchText.toLowerCase(); | ||||
|                 result = result.filter(monitor => { | ||||
|                     return monitor.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || tag.value?.toLowerCase().includes(loweredSearchText)); | ||||
|                         || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) | ||||
|                             || tag.value?.toLowerCase().includes(loweredSearchText)); | ||||
|                 }); | ||||
|             } else { | ||||
|                 result = result.filter(monitor => monitor.parent === null); | ||||
| @@ -105,6 +123,27 @@ export default { | ||||
|                 return m1.name.localeCompare(m2.name); | ||||
|             }); | ||||
|  | ||||
|             if (this.filterState.status != null && this.filterState.status.length > 0) { | ||||
|                 result.map(monitor => { | ||||
|                     if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) { | ||||
|                         monitor.status = this.$root.lastHeartbeatList[monitor.id].status; | ||||
|                     } | ||||
|                 }); | ||||
|                 result = result.filter(monitor => this.filterState.status.includes(monitor.status)); | ||||
|             } | ||||
|  | ||||
|             if (this.filterState.active != null && this.filterState.active.length > 0) { | ||||
|                 result = result.filter(monitor => this.filterState.active.includes(monitor.active)); | ||||
|             } | ||||
|  | ||||
|             if (this.filterState.tags != null && this.filterState.tags.length > 0) { | ||||
|                 result = result.filter(monitor => { | ||||
|                     return monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs | ||||
|                         .filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags | ||||
|                         .length > 0; | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|     }, | ||||
| @@ -134,7 +173,14 @@ export default { | ||||
|         /** Clear the search bar */ | ||||
|         clearSearchText() { | ||||
|             this.searchText = ""; | ||||
|         } | ||||
|         }, | ||||
|         /** | ||||
|          * Update the MonitorList Filter | ||||
|          * @param {object} newFilter Object with new filter | ||||
|          */ | ||||
|         updateFilter(newFilter) { | ||||
|             this.filterState = newFilter; | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| @@ -159,8 +205,6 @@ export default { | ||||
|     margin: -10px; | ||||
|     margin-bottom: 10px; | ||||
|     padding: 10px; | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|  | ||||
|     .dark & { | ||||
|         background-color: $dark-header-bg; | ||||
| @@ -168,6 +212,17 @@ export default { | ||||
|     } | ||||
| } | ||||
|  | ||||
| .header-top { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .header-filter { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| @media (max-width: 770px) { | ||||
|     .list-header { | ||||
|         margin: -20px; | ||||
| @@ -216,5 +271,4 @@ export default { | ||||
|     padding-left: 67px; | ||||
|     margin-top: 5px; | ||||
| } | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										284
									
								
								src/components/MonitorListFilter.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								src/components/MonitorListFilter.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | ||||
| <template> | ||||
|     <div class="px-2 pt-2 d-flex"> | ||||
|         <button | ||||
|             type="button" | ||||
|             :title="$t('Clear current filters')" | ||||
|             class="clear-filters-btn btn" | ||||
|             :class="{ 'active': numFiltersActive > 0}" | ||||
|             tabindex="0" | ||||
|             :disabled="numFiltersActive === 0" | ||||
|             @click="clearFilters" | ||||
|         > | ||||
|             <font-awesome-icon icon="stream" /> | ||||
|             <span v-if="numFiltersActive > 0" class="px-1 fw-bold">{{ numFiltersActive }}</span> | ||||
|             <font-awesome-icon v-if="numFiltersActive > 0" icon="times" /> | ||||
|         </button> | ||||
|         <MonitorListFilterDropdown | ||||
|             :filterActive="filterState.status?.length > 0" | ||||
|         > | ||||
|             <template #status> | ||||
|                 <Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" /> | ||||
|                 <span v-else> | ||||
|                     {{ $t('Status') }} | ||||
|                 </span> | ||||
|             </template> | ||||
|             <template #dropdown> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(1)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <Status :status="1" /> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.up }} | ||||
|                                 <span v-if="filterState.status?.includes(1)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(0)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <Status :status="0" /> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.down }} | ||||
|                                 <span v-if="filterState.status?.includes(0)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(2)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <Status :status="2" /> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.pending }} | ||||
|                                 <span v-if="filterState.status?.includes(2)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(3)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <Status :status="3" /> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.maintenance }} | ||||
|                                 <span v-if="filterState.status?.includes(3)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|             </template> | ||||
|         </MonitorListFilterDropdown> | ||||
|         <MonitorListFilterDropdown :filterActive="filterState.active?.length > 0"> | ||||
|             <template #status> | ||||
|                 <span v-if="filterState.active?.length === 1"> | ||||
|                     <span v-if="filterState.active[0]">{{ $t("Running") }}</span> | ||||
|                     <span v-else>{{ $t("filterActivePaused") }}</span> | ||||
|                 </span> | ||||
|                 <span v-else> | ||||
|                     {{ $t("filterActive") }} | ||||
|                 </span> | ||||
|             </template> | ||||
|             <template #dropdown> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(true)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <span>{{ $t("Running") }}</span> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.active }} | ||||
|                                 <span v-if="filterState.active?.includes(true)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <span>{{ $t("filterActivePaused") }}</span> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ $root.stats.pause }} | ||||
|                                 <span v-if="filterState.active?.includes(false)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|             </template> | ||||
|         </MonitorListFilterDropdown> | ||||
|         <MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0"> | ||||
|             <template #status> | ||||
|                 <Tag | ||||
|                     v-if="filterState.tags?.length === 1" | ||||
|                     :item="tagsList.find(tag => tag.id === filterState.tags[0])" | ||||
|                     :size="'sm'" | ||||
|                 /> | ||||
|                 <span v-else> | ||||
|                     {{ $t('Tags') }} | ||||
|                 </span> | ||||
|             </template> | ||||
|             <template #dropdown> | ||||
|                 <li v-for="tag in tagsList" :key="tag.id"> | ||||
|                     <div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)"> | ||||
|                         <div class="d-flex align-items-center justify-content-between"> | ||||
|                             <span><Tag :item="tag" :size="'sm'" /></span> | ||||
|                             <span class="ps-3"> | ||||
|                                 {{ getTaggedMonitorCount(tag) }} | ||||
|                                 <span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active"> | ||||
|                                     <font-awesome-icon icon="check" /> | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|             </template> | ||||
|         </MonitorListFilterDropdown> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import MonitorListFilterDropdown from "./MonitorListFilterDropdown.vue"; | ||||
| import Status from "./Status.vue"; | ||||
| import Tag from "./Tag.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         MonitorListFilterDropdown, | ||||
|         Status, | ||||
|         Tag, | ||||
|     }, | ||||
|     props: { | ||||
|         filterState: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|         } | ||||
|     }, | ||||
|     emits: [ "updateFilter" ], | ||||
|     data() { | ||||
|         return { | ||||
|             tagsList: [], | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         numFiltersActive() { | ||||
|             let num = 0; | ||||
|  | ||||
|             Object.values(this.filterState).forEach(item => { | ||||
|                 if (item != null && item.length > 0) { | ||||
|                     num += 1; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return num; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.getExistingTags(); | ||||
|     }, | ||||
|     methods: { | ||||
|         toggleStatusFilter(status) { | ||||
|             let newFilter = { | ||||
|                 ...this.filterState | ||||
|             }; | ||||
|  | ||||
|             if (newFilter.status == null) { | ||||
|                 newFilter.status = [ status ]; | ||||
|             } else { | ||||
|                 if (newFilter.status.includes(status)) { | ||||
|                     newFilter.status = newFilter.status.filter(item => item !== status); | ||||
|                 } else { | ||||
|                     newFilter.status.push(status); | ||||
|                 } | ||||
|             } | ||||
|             this.$emit("updateFilter", newFilter); | ||||
|         }, | ||||
|         toggleActiveFilter(active) { | ||||
|             let newFilter = { | ||||
|                 ...this.filterState | ||||
|             }; | ||||
|  | ||||
|             if (newFilter.active == null) { | ||||
|                 newFilter.active = [ active ]; | ||||
|             } else { | ||||
|                 if (newFilter.active.includes(active)) { | ||||
|                     newFilter.active = newFilter.active.filter(item => item !== active); | ||||
|                 } else { | ||||
|                     newFilter.active.push(active); | ||||
|                 } | ||||
|             } | ||||
|             this.$emit("updateFilter", newFilter); | ||||
|         }, | ||||
|         toggleTagFilter(tag) { | ||||
|             let newFilter = { | ||||
|                 ...this.filterState | ||||
|             }; | ||||
|  | ||||
|             if (newFilter.tags == null) { | ||||
|                 newFilter.tags = [ tag.id ]; | ||||
|             } else { | ||||
|                 if (newFilter.tags.includes(tag.id)) { | ||||
|                     newFilter.tags = newFilter.tags.filter(item => item !== tag.id); | ||||
|                 } else { | ||||
|                     newFilter.tags.push(tag.id); | ||||
|                 } | ||||
|             } | ||||
|             this.$emit("updateFilter", newFilter); | ||||
|         }, | ||||
|         clearFilters() { | ||||
|             this.$emit("updateFilter", { | ||||
|                 status: null, | ||||
|             }); | ||||
|         }, | ||||
|         getExistingTags() { | ||||
|             this.$root.getSocket().emit("getTags", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.tagsList = res.tags; | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         getTaggedMonitorCount(tag) { | ||||
|             return Object.values(this.$root.monitorList).filter(monitor => { | ||||
|                 return monitor.tags.find(monitorTag => monitorTag.tag_id === tag.id); | ||||
|             }).length; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .clear-filters-btn { | ||||
|     font-size: 0.8em; | ||||
|     margin-right: 5px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     padding: 2px 10px; | ||||
|     border-radius: 16px; | ||||
|     background-color: transparent; | ||||
|  | ||||
|     .dark & { | ||||
|         color: $dark-font-color; | ||||
|         border: 1px solid $dark-font-color2; | ||||
|     } | ||||
|  | ||||
|     &.active { | ||||
|         border: 1px solid $highlight; | ||||
|         background-color: $highlight-white; | ||||
|  | ||||
|         .dark & { | ||||
|             background-color: $dark-font-color2; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										131
									
								
								src/components/MonitorListFilterDropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/components/MonitorListFilterDropdown.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| <template> | ||||
|     <div class="dropdown" @focusin="open = true" @focusout="handleFocusOut"> | ||||
|         <button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0"> | ||||
|             <div class="px-1 d-flex align-items-center"> | ||||
|                 <slot name="status"></slot> | ||||
|             </div> | ||||
|             <span class="px-1"> | ||||
|                 <font-awesome-icon icon="angle-down" /> | ||||
|             </span> | ||||
|         </button> | ||||
|         <ul class="filter-dropdown-menu" :class="{ 'open': open }"> | ||||
|             <slot name="dropdown"></slot> | ||||
|         </ul> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|  | ||||
|     }, | ||||
|     props: { | ||||
|         filterActive: { | ||||
|             type: Boolean, | ||||
|             required: true, | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             open: false | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         handleFocusOut(e) { | ||||
|             if (e.relatedTarget != null && this.$el.contains(e.relatedTarget)) { | ||||
|                 return; | ||||
|             } | ||||
|             this.open = false; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss"> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .filter-dropdown-menu { | ||||
|     z-index: 100; | ||||
|     transition: all 0.2s; | ||||
|     padding: 5px 0 !important; | ||||
|     border-radius: 16px; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     position: absolute; | ||||
|     inset: 0 auto auto 0; | ||||
|     margin: 0; | ||||
|     transform: translate(0, 36px); | ||||
|     box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1); | ||||
|     visibility: hidden; | ||||
|     list-style: none; | ||||
|     height: 0; | ||||
|     opacity: 0; | ||||
|     background: white; | ||||
|  | ||||
|     &.open { | ||||
|         height: unset; | ||||
|         visibility: inherit; | ||||
|         opacity: 1; | ||||
|     } | ||||
|  | ||||
|     .dropdown-item { | ||||
|         padding: 5px 15px; | ||||
|     } | ||||
|  | ||||
|     .dropdown-item:focus { | ||||
|         background: $highlight-white; | ||||
|  | ||||
|         .dark & { | ||||
|             background: $dark-bg2; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .dark & { | ||||
|         background-color: $dark-bg; | ||||
|         color: $dark-font-color; | ||||
|         border-color: $dark-border-color; | ||||
|  | ||||
|         .dropdown-item { | ||||
|             color: $dark-font-color; | ||||
|  | ||||
|             &.active { | ||||
|                 color: $dark-font-color2; | ||||
|                 background-color: $highlight !important; | ||||
|             } | ||||
|  | ||||
|             &:hover { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .filter-dropdown-status { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     padding: 4px 10px; | ||||
|     margin-left: 5px; | ||||
|     border: 1px solid #ced4da; | ||||
|     border-radius: 25px; | ||||
|     background-color: transparent; | ||||
|  | ||||
|     .dark & { | ||||
|         color: $dark-font-color; | ||||
|         border: 1px solid $dark-font-color2; | ||||
|     } | ||||
|  | ||||
|     &.active { | ||||
|         border: 1px solid $highlight; | ||||
|         background-color: $highlight-white; | ||||
|  | ||||
|         .dark & { | ||||
|             background-color: $dark-font-color2; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .filter-active { | ||||
|     color: $highlight; | ||||
| } | ||||
| </style> | ||||
| @@ -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; | ||||
|         }, | ||||
|   | ||||
| @@ -99,7 +99,7 @@ | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="modal-footer"> | ||||
|                         <button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> | ||||
|                         <button v-if="tag && tag.id !== null" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> | ||||
|                             {{ $t("Delete") }} | ||||
|                         </button> | ||||
|                         <button type="submit" class="btn btn-primary" :disabled="processing"> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|         <label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span> | ||||
|         <select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select"> | ||||
|             <option value="ios">iOS</option> | ||||
|             <option value="android">{{ $t("Android") }}</option> | ||||
|             <option value="android">Android</option> | ||||
|             <option value="huawei">{{ $t("Huawei") }}</option> | ||||
|         </select> | ||||
|     </div> | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|         <div class="form-text"> | ||||
|             <span style="color: red;"><sup>*</sup></span>{{ $t("Required") }} | ||||
|             <i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;"> | ||||
|                 <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a> | ||||
|                 <a href="https://developers.mattermost.com/integrate/webhooks/incoming/" target="_blank">https://developers.mattermost.com/integrate/webhooks/incoming/</a> | ||||
|             </i18n-t> | ||||
|             <p style="margin-top: 8px;"> | ||||
|                 {{ $t("aboutMattermostChannelName") }} | ||||
|   | ||||
| @@ -7,8 +7,9 @@ | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|         <label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label> | ||||
|         <div class="input-group mb-3"> | ||||
|             <input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required> | ||||
|         <input id="ntfy-server-url" v-model="$parent.notification.ntfyserverurl" type="text" class="form-control" required> | ||||
|         <div class="form-text"> | ||||
|             {{ $t("Server URL should not contain the nfty topic") }} | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|   | ||||
							
								
								
									
										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, | ||||
|   | ||||
| @@ -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> | ||||
| @@ -455,8 +455,6 @@ | ||||
|     "For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري", | ||||
|     "Device Token": "رمز الجهاز", | ||||
|     "Platform": "منصة", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "ذكري المظهر", | ||||
|     "Huawei": "هواوي", | ||||
|     "High": "عالٍ", | ||||
|     "Retry": "إعادة المحاولة", | ||||
|   | ||||
| @@ -592,7 +592,6 @@ | ||||
|     "For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري", | ||||
|     "Device Token": "رمز الجهاز", | ||||
|     "Platform": "منصة", | ||||
|     "Android": "ذكري المظهر", | ||||
|     "Huawei": "هواوي", | ||||
|     "High": "عالٍ", | ||||
|     "Retry": "إعادة المحاولة", | ||||
|   | ||||
| @@ -396,8 +396,6 @@ | ||||
|     "For safety, must use secret key": "За сигурност, трябва да се използва таен ключ", | ||||
|     "Device Token": "Токен за устройство", | ||||
|     "Platform": "Платформа", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Висок", | ||||
|     "Retry": "Повтори", | ||||
|   | ||||
| @@ -454,8 +454,6 @@ | ||||
|     "For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key", | ||||
|     "Device Token": "Token zařízení", | ||||
|     "Platform": "Platforma", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Vysoký", | ||||
|     "Retry": "Opakovat", | ||||
|   | ||||
| @@ -558,7 +558,6 @@ | ||||
|     "high": "høj", | ||||
|     "Base URL": "Base URL", | ||||
|     "Platform": "Platform", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "Retry": "Forsøg igen", | ||||
|     "Topic": "Emne", | ||||
|   | ||||
| @@ -403,8 +403,6 @@ | ||||
|     "For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden", | ||||
|     "Device Token": "Gerätetoken", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Hoch", | ||||
|     "Retry": "Wiederholungen", | ||||
|   | ||||
| @@ -403,8 +403,6 @@ | ||||
|     "For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden", | ||||
|     "Device Token": "Gerätetoken", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Hoch", | ||||
|     "Retry": "Wiederholungen", | ||||
|   | ||||
| @@ -420,8 +420,6 @@ | ||||
|     "For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key", | ||||
|     "Device Token": "Device Token", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "Ξαναδοκιμάσετε", | ||||
|   | ||||
| @@ -56,6 +56,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", | ||||
| @@ -157,6 +160,8 @@ | ||||
|     "Disable 2FA": "Disable 2FA", | ||||
|     "2FA Settings": "2FA Settings", | ||||
|     "Two Factor Authentication": "Two Factor Authentication", | ||||
|     "filterActive": "Active", | ||||
|     "filterActivePaused": "Paused", | ||||
|     "Active": "Active", | ||||
|     "Inactive": "Inactive", | ||||
|     "Token": "Token", | ||||
| @@ -200,8 +205,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", | ||||
| @@ -361,6 +369,7 @@ | ||||
|     "deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?", | ||||
|     "socket": "Socket", | ||||
|     "tcp": "TCP / HTTP", | ||||
|     "tailscalePingWarning": "In order to use the Tailscale Ping monitor, you need to install Uptime Kuma without Docker and also install Tailscale client on your server.", | ||||
|     "Docker Container": "Docker Container", | ||||
|     "Container Name / ID": "Container Name / ID", | ||||
|     "Docker Host": "Docker Host", | ||||
| @@ -523,6 +532,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.", | ||||
| @@ -614,7 +625,6 @@ | ||||
|     "For safety, must use secret key": "For safety, must use secret key", | ||||
|     "Device Token": "Device Token", | ||||
|     "Platform": "Platform", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "Retry", | ||||
| @@ -637,6 +647,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!", | ||||
| @@ -683,6 +695,7 @@ | ||||
|     "Octopush API Version": "Octopush API Version", | ||||
|     "Legacy Octopush-DM": "Legacy Octopush-DM", | ||||
|     "ntfy Topic": "ntfy Topic", | ||||
|     "Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic", | ||||
|     "onebotHttpAddress": "OneBot HTTP Address", | ||||
|     "onebotMessageType": "OneBot Message Type", | ||||
|     "onebotGroupMessage": "Group", | ||||
| @@ -730,7 +743,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", | ||||
| @@ -739,13 +753,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", | ||||
| @@ -759,6 +774,21 @@ | ||||
|     "Badge URL": "Badge URL", | ||||
|     "Group": "Group", | ||||
|     "Monitor Group": "Monitor Group", | ||||
|     "Kafka Brokers": "Kafka Brokers", | ||||
|     "Enter the list of brokers": "Enter the list of brokers", | ||||
|     "Press Enter to add broker": "Press Enter to add broker", | ||||
|     "Kafka Topic Name": "Kafka Topic Name", | ||||
|     "Kafka Producer Message": "Kafka Producer Message", | ||||
|     "Enable Kafka SSL": "Enable Kafka SSL", | ||||
|     "Enable Kafka Producer Auto Topic Creation": "Enable Kafka Producer Auto Topic Creation", | ||||
|     "Kafka SASL Options": "Kafka SASL Options", | ||||
|     "Mechanism": "Mechanism", | ||||
|     "Pick a SASL Mechanism...": "Pick a SASL Mechanism...", | ||||
|     "Authorization Identity": "Authorization Identity", | ||||
|     "AccessKey Id": "AccessKey Id", | ||||
|     "Secret AccessKey": "Secret AccessKey", | ||||
|     "Session Token": "Session Token", | ||||
|     "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.", | ||||
|     "Close": "Close" | ||||
|     "Close": "Close", | ||||
|     "Request Body": "Request Body" | ||||
| } | ||||
|   | ||||
| @@ -497,8 +497,6 @@ | ||||
|     "Proto Method": "Método Proto", | ||||
|     "Proto Content": "Contenido Proto", | ||||
|     "Economy": "Económico", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Platform": "Plataforma", | ||||
|     "onebotPrivateMessage": "Privado", | ||||
|     "onebotMessageType": "Tipo de Mensaje OneBot", | ||||
|   | ||||
| @@ -415,8 +415,6 @@ | ||||
|     "For safety, must use secret key": "For safety, must use secret key", | ||||
|     "Device Token": "Gailu tokena", | ||||
|     "Platform": "Plataforma", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Altua", | ||||
|     "Retry": "Errepikatu", | ||||
|   | ||||
| @@ -568,7 +568,6 @@ | ||||
|     "SendKey": "کلید ارسال (SendKey)", | ||||
|     "SecretAccessKey": "کلید دسترسی مخفی (AccessKey Secret)", | ||||
|     "SignName": "نام امضا (SignName)", | ||||
|     "Android": "اندروید", | ||||
|     "Huawei": "هواوی", | ||||
|     "WeCom Bot Key": "کلید ربات WeCom", | ||||
|     "Setup Proxy": "تنظیم پروکسی", | ||||
|   | ||||
| @@ -547,7 +547,6 @@ | ||||
|     "For safety, must use secret key": "Turvallisuuden vuoksi on käytettävä salaista avainta", | ||||
|     "Device Token": "Laitteen tunnus", | ||||
|     "Platform": "Alusta", | ||||
|     "iOS": "iOS", | ||||
|     "Bark Endpoint": "Bark päätepiste", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Korkea", | ||||
| @@ -564,7 +563,6 @@ | ||||
|     "promosmsAllowLongSMS": "Salli pitkät tekstiviestit", | ||||
|     "Feishu WebHookUrl": "Feishu WebHookURL-osoite", | ||||
|     "Internal Room Id": "Huoneen sisäinen tunnus", | ||||
|     "Android": "Android", | ||||
|     "Channel Name": "Kanavan nimi", | ||||
|     "Uptime Kuma URL": "Uptime Kuma URL-osoite", | ||||
|     "Icon Emoji": "Ikoni Emoji", | ||||
|   | ||||
| @@ -451,8 +451,6 @@ | ||||
|     "For safety, must use secret key": "Par sécurité, utilisation obligatoire de la clé secrète", | ||||
|     "Device Token": "Jeton d'appareil", | ||||
|     "Platform": "Plateforme", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Haute", | ||||
|     "Retry": "Recommencez", | ||||
|   | ||||
| @@ -445,8 +445,6 @@ | ||||
|     "For safety, must use secret key": "לבטיחות, חייב להשתמש במפתח סודיy", | ||||
|     "Device Token": "אסימון מכשיר", | ||||
|     "Platform": "פּלַטפוֹרמָה", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "דְמוּי אָדָם", | ||||
|     "Huawei": "huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "נסה שוב", | ||||
|   | ||||
| @@ -420,8 +420,6 @@ | ||||
|     "For safety, must use secret key": "Korištenje tajnog ključa je obavezno", | ||||
|     "Device Token": "Token uređaja", | ||||
|     "Platform": "Platforma", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Visoko", | ||||
|     "Retry": "Ponovnih pokušaja", | ||||
|   | ||||
| @@ -418,8 +418,6 @@ | ||||
|     "For safety, must use secret key": "Untuk keamaan Anda harus menggunakan kunci rahasia", | ||||
|     "Device Token": "Token Perangkat", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Tinggi", | ||||
|     "Retry": "Ulang", | ||||
|   | ||||
| @@ -507,7 +507,6 @@ | ||||
|     "lineDevConsoleTo": "Line Developers Console - {0}", | ||||
|     "Basic Settings": "基本設定", | ||||
|     "User ID": "User ID", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "Device Token": "デバイストークン", | ||||
|     "recurringIntervalMessage": "毎日1回実行する|{0} 日に1回実行する", | ||||
|   | ||||
| @@ -413,8 +413,6 @@ | ||||
|     "For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.", | ||||
|     "Device Token": "기기 Token", | ||||
|     "Platform": "플랫폼", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "재시도", | ||||
|   | ||||
| @@ -404,8 +404,6 @@ | ||||
|     "For safety, must use secret key": "Voor de veiligheid moet je de secret key gebruiken", | ||||
|     "Device Token": "Apparaat Token", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Hoog", | ||||
|     "Retry": "Opnieuw", | ||||
|   | ||||
| @@ -414,8 +414,6 @@ | ||||
|     "For safety, must use secret key": "Ze względów bezpieczeństwa musisz użyć tajnego klucza", | ||||
|     "Device Token": "Token urządzenia", | ||||
|     "Platform": "Platforma", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Wysoki", | ||||
|     "Retry": "Ponów", | ||||
|   | ||||
| @@ -523,7 +523,6 @@ | ||||
|     "Example:": "Exemplo: {0}", | ||||
|     "Read more:": "Leia mais em: {0}", | ||||
|     "promosmsAllowLongSMS": "Permitir SMS grandes", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "smseagleTo": "Números Dos Telefones", | ||||
|     "smseaglePriority": "Prioridade da mensagem (0-9, padrão=0)", | ||||
|   | ||||
| @@ -421,8 +421,6 @@ | ||||
|     "For safety, must use secret key": "В целях безопасности необходимо использовать секретный ключ", | ||||
|     "Device Token": "Токен устройства", | ||||
|     "Platform": "Платформа", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "Повторить", | ||||
|   | ||||
| @@ -404,8 +404,6 @@ | ||||
|     "For safety, must use secret key": "เพื่อความปลอดภัย จำเป็นต้องตั้งค่ากุญแจการเข้าถึง", | ||||
|     "Device Token": "Device Token", | ||||
|     "Platform": "แพลตฟอร์ม", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "สูง", | ||||
|     "Retry": "ลองใหม่", | ||||
|   | ||||
| @@ -408,8 +408,6 @@ | ||||
|     "For safety, must use secret key": "Güvenlik için gizli anahtar kullanılmalıdır", | ||||
|     "Device Token": "Cihaz Tokeni", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "Tekrar", | ||||
|   | ||||
| @@ -413,8 +413,6 @@ | ||||
|     "For safety, must use secret key": "Для безпеки необхідно використовувати секретний ключ", | ||||
|     "Device Token": "Токен пристрою", | ||||
|     "Platform": "Платформа", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "Високий", | ||||
|     "Retry": "Повтор", | ||||
|   | ||||
| @@ -403,8 +403,6 @@ | ||||
|     "For safety, must use secret key": "Để an toàn, hãy dùng secret key", | ||||
|     "Device Token": "Device Token", | ||||
|     "Platform": "Platform", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "Huawei", | ||||
|     "High": "High", | ||||
|     "Retry": "Retry", | ||||
|   | ||||
| @@ -452,8 +452,6 @@ | ||||
|     "For safety, must use secret key": "出于安全考虑,必须使用加签密钥", | ||||
|     "Device Token": "Apple Device Token", | ||||
|     "Platform": "平台", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "华为", | ||||
|     "High": "高", | ||||
|     "Retry": "重试次数", | ||||
|   | ||||
| @@ -139,6 +139,8 @@ | ||||
|     "Disable 2FA": "關閉 2FA", | ||||
|     "2FA Settings": "2FA 設定", | ||||
|     "Two Factor Authentication": "雙重認證", | ||||
|     "filterActive": "執行狀態", | ||||
|     "filterActivePaused": "已暫停", | ||||
|     "Active": "生效", | ||||
|     "Inactive": "未生效", | ||||
|     "Token": "Token", | ||||
| @@ -692,7 +694,6 @@ | ||||
|     "Retry": "重試", | ||||
|     "High": "高", | ||||
|     "Huawei": "華為", | ||||
|     "Android": "Android", | ||||
|     "For safety, must use secret key": "為安全起見,必須使用 Secret Key", | ||||
|     "SecretKey": "SecretKey", | ||||
|     "WebHookUrl": "WebHookUrl", | ||||
|   | ||||
| @@ -445,8 +445,6 @@ | ||||
|     "For safety, must use secret key": "為了安全起見,必須使用秘密金鑰", | ||||
|     "Device Token": "裝置權杖", | ||||
|     "Platform": "平台", | ||||
|     "iOS": "iOS", | ||||
|     "Android": "Android", | ||||
|     "Huawei": "華為", | ||||
|     "High": "高", | ||||
|     "Retry": "重試", | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import axios from "axios"; | ||||
| import { getDevContainerServerHostname, isDevContainer } from "../util-frontend"; | ||||
|  | ||||
| const env = process.env.NODE_ENV || "production"; | ||||
|  | ||||
| // change the axios base url for development | ||||
| if (env === "development" || localStorage.dev === "dev") { | ||||
| if (env === "development" && isDevContainer()) { | ||||
|     axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname(); | ||||
| } else if (env === "development" || localStorage.dev === "dev") { | ||||
|     axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import jwtDecode from "jwt-decode"; | ||||
| import Favico from "favico.js"; | ||||
| import dayjs from "dayjs"; | ||||
| import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; | ||||
| import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js"; | ||||
| const toast = useToast(); | ||||
|  | ||||
| let socket; | ||||
| @@ -98,7 +99,9 @@ export default { | ||||
|  | ||||
|             let wsHost; | ||||
|             const env = process.env.NODE_ENV || "production"; | ||||
|             if (env === "development" || localStorage.dev === "dev") { | ||||
|             if (env === "development" && isDevContainer()) { | ||||
|                 wsHost = protocol + getDevContainerServerHostname(); | ||||
|             } else if (env === "development" || localStorage.dev === "dev") { | ||||
|                 wsHost = protocol + location.hostname + ":3001"; | ||||
|             } else { | ||||
|                 wsHost = protocol + location.host; | ||||
| @@ -698,9 +701,11 @@ export default { | ||||
|  | ||||
|         stats() { | ||||
|             let result = { | ||||
|                 active: 0, | ||||
|                 up: 0, | ||||
|                 down: 0, | ||||
|                 maintenance: 0, | ||||
|                 pending: 0, | ||||
|                 unknown: 0, | ||||
|                 pause: 0, | ||||
|             }; | ||||
| @@ -712,12 +717,13 @@ export default { | ||||
|                 if (monitor && ! monitor.active) { | ||||
|                     result.pause++; | ||||
|                 } else if (beat) { | ||||
|                     result.active++; | ||||
|                     if (beat.status === UP) { | ||||
|                         result.up++; | ||||
|                     } else if (beat.status === DOWN) { | ||||
|                         result.down++; | ||||
|                     } else if (beat.status === PENDING) { | ||||
|                         result.up++; | ||||
|                         result.pending++; | ||||
|                     } else if (beat.status === MAINTENANCE) { | ||||
|                         result.maintenance++; | ||||
|                     } else { | ||||
|   | ||||
| @@ -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> | ||||
| @@ -432,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"); | ||||
|             } | ||||
|  | ||||
| @@ -582,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> | ||||
| @@ -58,6 +61,9 @@ | ||||
|                                         <option value="mqtt"> | ||||
|                                             MQTT | ||||
|                                         </option> | ||||
|                                         <option value="kafka-producer"> | ||||
|                                             Kafka Producer | ||||
|                                         </option> | ||||
|                                         <option value="sqlserver"> | ||||
|                                             Microsoft SQL Server | ||||
|                                         </option> | ||||
| @@ -76,10 +82,17 @@ | ||||
|                                         <option value="redis"> | ||||
|                                             Redis | ||||
|                                         </option> | ||||
|                                         <option value="tailscale-ping"> | ||||
|                                             Tailscale Ping | ||||
|                                         </option> | ||||
|                                     </optgroup> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|  | ||||
|                             <div v-if="monitor.type === 'tailscale-ping'" class="alert alert-warning" role="alert"> | ||||
|                                 {{ $t("tailscalePingWarning") }} | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Friendly Name --> | ||||
|                             <div class="my-3"> | ||||
|                                 <label for="name" class="form-label">{{ $t("Friendly Name") }}</label> | ||||
| @@ -97,7 +110,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- URL --> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'real-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> | ||||
| @@ -127,6 +140,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"> | ||||
| @@ -138,9 +176,60 @@ | ||||
|                                 </select> | ||||
|                             </div> | ||||
|  | ||||
|                             <template v-if="monitor.type === 'kafka-producer'"> | ||||
|                                 <!-- Kafka Brokers List --> | ||||
|                                 <div class="my-3"> | ||||
|                                     <label for="kafkaProducerBrokers" class="form-label">{{ $t("Kafka Brokers") }}</label> | ||||
|                                     <VueMultiselect | ||||
|                                         id="kafkaProducerBrokers" | ||||
|                                         v-model="monitor.kafkaProducerBrokers" | ||||
|                                         :multiple="true" | ||||
|                                         :options="[]" | ||||
|                                         :placeholder="$t('Enter the list of brokers')" | ||||
|                                         :tag-placeholder="$t('Press Enter to add broker')" | ||||
|                                         :max-height="500" | ||||
|                                         :taggable="true" | ||||
|                                         :show-no-options="false" | ||||
|                                         :close-on-select="false" | ||||
|                                         :clear-on-select="false" | ||||
|                                         :preserve-search="false" | ||||
|                                         :preselect-first="false" | ||||
|                                         @tag="addKafkaProducerBroker" | ||||
|                                     ></VueMultiselect> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Kafka Topic Name --> | ||||
|                                 <div class="my-3"> | ||||
|                                     <label for="kafkaProducerTopic" class="form-label">{{ $t("Kafka Topic Name") }}</label> | ||||
|                                     <input id="kafkaProducerTopic" v-model="monitor.kafkaProducerTopic" type="text" class="form-control" required> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Kafka Producer Message --> | ||||
|                                 <div class="my-3"> | ||||
|                                     <label for="kafkaProducerMessage" class="form-label">{{ $t("Kafka Producer Message") }}</label> | ||||
|                                     <input id="kafkaProducerMessage" v-model="monitor.kafkaProducerMessage" type="text" class="form-control" required> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Kafka SSL --> | ||||
|                                 <div class="my-3 form-check"> | ||||
|                                     <input id="kafkaProducerSsl" v-model="monitor.kafkaProducerSsl" class="form-check-input" type="checkbox"> | ||||
|                                     <label class="form-check-label" for="kafkaProducerSsl"> | ||||
|                                         {{ $t("Enable Kafka SSL") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <!-- Kafka SSL --> | ||||
|                                 <div class="my-3 form-check"> | ||||
|                                     <input id="kafkaProducerAllowAutoTopicCreation" v-model="monitor.kafkaProducerAllowAutoTopicCreation" class="form-check-input" type="checkbox"> | ||||
|                                     <label class="form-check-label" for="kafkaProducerAllowAutoTopicCreation"> | ||||
|                                         {{ $t("Enable Kafka Producer Auto Topic Creation") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|  | ||||
|                             <!-- Hostname --> | ||||
|                             <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only --> | ||||
|                             <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3"> | ||||
|                             <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only --> | ||||
|                             <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping'" class="my-3"> | ||||
|                                 <label for="hostname" class="form-label">{{ $t("Hostname") }}</label> | ||||
|                                 <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required> | ||||
|                             </div> | ||||
| @@ -356,7 +445,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") }} | ||||
| @@ -365,7 +454,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") }} | ||||
| @@ -457,7 +546,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.") }} | ||||
| @@ -484,8 +573,58 @@ | ||||
|                                 </button> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Kafka SASL Options --> | ||||
|                             <!-- Kafka Producer only --> | ||||
|                             <template v-if="monitor.type === 'kafka-producer'"> | ||||
|                                 <h2 class="mt-5 mb-2">{{ $t("Kafka SASL Options") }}</h2> | ||||
|                                 <div class="my-3"> | ||||
|                                     <label class="form-label" for="kafkaProducerSaslMechanism"> | ||||
|                                         {{ $t("Mechanism") }} | ||||
|                                     </label> | ||||
|                                     <VueMultiselect | ||||
|                                         id="kafkaProducerSaslMechanism" | ||||
|                                         v-model="monitor.kafkaProducerSaslOptions.mechanism" | ||||
|                                         :options="kafkaSaslMechanismOptions" | ||||
|                                         :multiple="false" | ||||
|                                         :clear-on-select="false" | ||||
|                                         :preserve-search="false" | ||||
|                                         :placeholder="$t('Pick a SASL Mechanism...')" | ||||
|                                         :preselect-first="false" | ||||
|                                         :max-height="500" | ||||
|                                         :allow-empty="false" | ||||
|                                         :taggable="false" | ||||
|                                     ></VueMultiselect> | ||||
|                                 </div> | ||||
|                                 <div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'None'"> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslUsername" class="form-label">{{ $t("Username") }}</label> | ||||
|                                         <input id="kafkaProducerSaslUsername" v-model="monitor.kafkaProducerSaslOptions.username" type="text" autocomplete="kafkaProducerSaslUsername" class="form-control"> | ||||
|                                     </div> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslPassword" class="form-label">{{ $t("Password") }}</label> | ||||
|                                         <input id="kafkaProducerSaslPassword" v-model="monitor.kafkaProducerSaslOptions.password" type="password" autocomplete="kafkaProducerSaslPassword" class="form-control"> | ||||
|                                     </div> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslAuthorizationIdentity" class="form-label">{{ $t("Authorization Identity") }}</label> | ||||
|                                         <input id="kafkaProducerSaslAuthorizationIdentity" v-model="monitor.kafkaProducerSaslOptions.authorizationIdentity" type="text" autocomplete="kafkaProducerSaslAuthorizationIdentity" class="form-control" required> | ||||
|                                     </div> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslAccessKeyId" class="form-label">{{ $t("AccessKey Id") }}</label> | ||||
|                                         <input id="kafkaProducerSaslAccessKeyId" v-model="monitor.kafkaProducerSaslOptions.accessKeyId" type="text" autocomplete="kafkaProducerSaslAccessKeyId" class="form-control" required> | ||||
|                                     </div> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslSecretAccessKey" class="form-label">{{ $t("Secret AccessKey") }}</label> | ||||
|                                         <input id="kafkaProducerSaslSecretAccessKey" v-model="monitor.kafkaProducerSaslOptions.secretAccessKey" type="password" autocomplete="kafkaProducerSaslSecretAccessKey" class="form-control" required> | ||||
|                                     </div> | ||||
|                                     <div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3"> | ||||
|                                         <label for="kafkaProducerSaslSessionToken" class="form-label">{{ $t("Session Token") }}</label> | ||||
|                                         <input id="kafkaProducerSaslSessionToken" v-model="monitor.kafkaProducerSaslOptions.sessionToken" type="password" autocomplete="kafkaProducerSaslSessionToken" class="form-control"> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|  | ||||
|                             <!-- 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 --> | ||||
| @@ -696,6 +835,7 @@ export default { | ||||
|             }, | ||||
|             acceptedStatusCodeOptions: [], | ||||
|             dnsresolvetypeOptions: [], | ||||
|             kafkaSaslMechanismOptions: [], | ||||
|             ipOrHostnameRegexPattern: hostNameRegexPattern(), | ||||
|             mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true), | ||||
|             gameList: null, | ||||
| @@ -959,12 +1099,21 @@ message HealthCheckResponse { | ||||
|             "TXT", | ||||
|         ]; | ||||
|  | ||||
|         let kafkaSaslMechanismOptions = [ | ||||
|             "None", | ||||
|             "plain", | ||||
|             "scram-sha-256", | ||||
|             "scram-sha-512", | ||||
|             "aws", | ||||
|         ]; | ||||
|  | ||||
|         for (let i = 100; i <= 999; i++) { | ||||
|             acceptedStatusCodeOptions.push(i.toString()); | ||||
|         } | ||||
|  | ||||
|         this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; | ||||
|         this.dnsresolvetypeOptions = dnsresolvetypeOptions; | ||||
|         this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions; | ||||
|     }, | ||||
|     methods: { | ||||
|         /** Initialize the edit monitor form */ | ||||
| @@ -998,7 +1147,11 @@ message HealthCheckResponse { | ||||
|                     mqttTopic: "", | ||||
|                     mqttSuccessMessage: "", | ||||
|                     authMethod: null, | ||||
|                     httpBodyEncoding: "json" | ||||
|                     httpBodyEncoding: "json", | ||||
|                     kafkaProducerBrokers: [], | ||||
|                     kafkaProducerSaslOptions: { | ||||
|                         mechanism: "None", | ||||
|                     }, | ||||
|                 }; | ||||
|  | ||||
|                 if (this.$root.proxyList && !this.monitor.proxyId) { | ||||
| @@ -1039,6 +1192,7 @@ message HealthCheckResponse { | ||||
|                             this.monitor.childrenIDs = undefined; | ||||
|                             this.monitor.forceInactive = undefined; | ||||
|                             this.monitor.pathName = undefined; | ||||
|                             this.monitor.screenshot = undefined; | ||||
|  | ||||
|                             this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]); | ||||
|                             this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => { | ||||
| @@ -1065,6 +1219,10 @@ message HealthCheckResponse { | ||||
|  | ||||
|         }, | ||||
|  | ||||
|         addKafkaProducerBroker(newBroker) { | ||||
|             this.monitor.kafkaProducerBrokers.push(newBroker); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Validate form input | ||||
|          * @returns {boolean} Is the form input valid? | ||||
| @@ -1107,7 +1265,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"; | ||||
| import SetupDatabase from "./pages/SetupDatabase.vue"; | ||||
|  | ||||
| // Settings - Sub Pages | ||||
| @@ -131,10 +130,6 @@ const routes = [ | ||||
|                                 path: "backup", | ||||
|                                 component: Backup, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "plugins", | ||||
|                                 component: Plugins, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "about", | ||||
|                                 component: About, | ||||
|   | ||||
| @@ -72,13 +72,32 @@ export function setPageLocale() { | ||||
|  */ | ||||
| export function getResBaseURL() { | ||||
|     const env = process.env.NODE_ENV; | ||||
|     if (env === "development" || localStorage.dev === "dev") { | ||||
|     if (env === "development" && isDevContainer()) { | ||||
|         return location.protocol + "//" + getDevContainerServerHostname(); | ||||
|     } else if (env === "development" || localStorage.dev === "dev") { | ||||
|         return location.protocol + "//" + location.hostname + ":3001"; | ||||
|     } else { | ||||
|         return ""; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function isDevContainer() { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1"); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Supports GitHub Codespaces only currently | ||||
|  */ | ||||
| export function getDevContainerServerHostname() { | ||||
|     if (!isDevContainer()) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     // eslint-disable-next-line no-undef | ||||
|     return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user