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
pull/3529/head^2
Louis Lam 1 year ago
commit a0bd4b248b

@ -0,0 +1,28 @@
# Codespaces
You can modifiy Uptime Kuma in your browser without setting up a local development.
![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595)
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
![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f)
## 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`
![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1)

@ -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]
}

@ -44,7 +44,7 @@ body:
id: operating-system id: operating-system
attributes: attributes:
label: "💻 Operating System and Arch" 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" placeholder: "Ex. Ubuntu 20.04 x86"
validations: validations:
required: true required: true
@ -52,7 +52,7 @@ body:
id: browser-vendor id: browser-vendor
attributes: attributes:
label: "🌐 Browser" 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" placeholder: "Ex. Google Chrome 95.0.4638.69"
validations: validations:
required: true required: true

@ -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 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Auto Test name: Auto Test
@ -33,7 +33,6 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install npm@latest -g - run: npm install npm@latest -g
- run: npm install - run: npm install
- run: npm run build - run: npm run build
@ -51,7 +50,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ARMv7 ] 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/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
@ -62,7 +61,6 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install npm@latest -g - run: npm install npm@latest -g
- run: npm ci --production - run: npm ci --production
@ -77,7 +75,6 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14 node-version: 14
cache: 'npm'
- run: npm install - run: npm install
- run: npm run lint - run: npm run lint
@ -92,7 +89,6 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14 node-version: 14
cache: 'npm'
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- run: npm run cy:test - run: npm run cy:test
@ -108,7 +104,6 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14 node-version: 14
cache: 'npm'
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- run: npm run cy:run:unit - run: npm run cy:run:unit

@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
## ⭐ Features ## ⭐ 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 * 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) * 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 * 20 second intervals
@ -54,7 +54,7 @@ Requirements:
- ✅ 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 - ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ Replit / Heroku - ❌ 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 - [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 - [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background

@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer"; import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression"; import viteCompression from "vite-plugin-compression";
import commonjs from "vite-plugin-commonjs";
const postCssScss = require("postcss-scss"); const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss"); const postcssRTLCSS = require("postcss-rtlcss");
@ -16,8 +17,12 @@ export default defineConfig({
}, },
define: { define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), "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: [ plugins: [
commonjs(),
vue(), vue(),
legacy({ legacy({
targets: [ "since 2015" ], targets: [ "since 2015" ],
@ -42,6 +47,9 @@ export default defineConfig({
} }
}, },
build: { build: {
commonjsOptions: {
include: [ /.js$/ ],
},
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id, { getModuleInfo, getModuleIds }) { manualChunks(id, { getModuleInfo, getModuleIds }) {

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

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

@ -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 FROM node:18-bullseye-slim AS base2-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt update && \ WORKDIR /app
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 && \ # Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
pip3 --no-cache-dir install apprise==1.4.0 && \ # 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/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove
# Install cloudflared # Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \ 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 && \ 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/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove
# For nscd
COPY ./docker/etc/nscd.conf /etc/nscd.conf
COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image # Full Base Image
# MariaDB, Chromium and fonts # MariaDB, Chromium and fonts
# Not working for armv7, so use the older version (10.5) of MariaDB from the debian repo # 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/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove && \ apt --yes autoremove && \
chown -R node:node /var/lib/mysql chown -R node:node /var/lib/mysql

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

@ -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 // curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
println("====================="); println("=====================");
println("Uptime Kuma Installer"); println("Uptime Kuma Install Script");
println("====================="); 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("---------------------------------------");
println("This script is designed for Linux and basic usage."); 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("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
println("---------------------------------------"); println("---------------------------------------");
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("Docker - Install Uptime Kuma Docker container");
println(""); println("");
@ -29,14 +29,10 @@ function checkNode() {
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')"); bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
println("Node Version: " ++ nodeVersion); println("Node Version: " ++ nodeVersion);
if (nodeVersion < "12") { if (nodeVersion <= "12") {
println("Error: Required Node.js 14"); println("Error: Required Node.js 14");
call("exit", "1"); call("exit", "1");
} }
if (nodeVersion == "12") {
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
}
} }
function deb() { function deb() {
@ -60,8 +56,8 @@ function deb() {
bash("apt --yes install curl"); bash("apt --yes install curl");
} }
println("Installing Node.js 14"); println("Installing Node.js 16");
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt"); bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
bash("apt --yes install nodejs"); bash("apt --yes install nodejs");
bash("node -v"); bash("node -v");
@ -91,6 +87,10 @@ if (type == "local") {
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')"); bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
if (os == "Ubuntu") { if (os == "Ubuntu") {
distribution = "ubuntu"; distribution = "ubuntu";
// Get ubuntu version
bash(". /etc/lsb-release");
version = DISTRIB_RELEASE;
} }
if (os == "Debian") { if (os == "Debian") {
distribution = "debian"; distribution = "debian";
@ -101,6 +101,7 @@ if (type == "local") {
println("Your OS: " ++ os); println("Your OS: " ++ os);
println("Distribution: " ++ distribution); println("Distribution: " ++ distribution);
println("Version: " ++ version);
println("Arch: " ++ arch); println("Arch: " ++ arch);
if ("$3" != "") { if ("$3" != "") {
@ -131,15 +132,32 @@ if (type == "local") {
checkNode(); checkNode();
} else { } else {
bash("curlCheck=$(curl --version)"); bash("dnfCheck=$(dnf --version)");
if (curlCheck == "") {
println("Installing Curl"); // Use yum
bash("yum -y -q install curl"); 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("node -v");
bash("nodeCheckAgain=$(node -v)"); bash("nodeCheckAgain=$(node -v)");
@ -193,6 +211,14 @@ if (type == "local") {
bash("pm2 startup"); 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("mkdir -p $installPath");
bash("cd $installPath"); bash("cd $installPath");
bash("git clone https://github.com/louislam/uptime-kuma.git ."); bash("git clone https://github.com/louislam/uptime-kuma.git .");

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

@ -3,15 +3,15 @@
# The command is working on Windows PowerShell and Docker for Windows only. # 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 # curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
"echo" "-e" "=====================" "echo" "-e" "====================="
"echo" "-e" "Uptime Kuma Installer" "echo" "-e" "Uptime Kuma Install Script"
"echo" "-e" "=====================" "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" "---------------------------------------"
"echo" "-e" "This script is designed for Linux and basic usage." "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" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
"echo" "-e" "---------------------------------------" "echo" "-e" "---------------------------------------"
"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" "Docker - Install Uptime Kuma Docker container"
"echo" "-e" "" "echo" "-e" ""
if [ "$1" != "" ]; then if [ "$1" != "" ]; then
@ -25,12 +25,9 @@ function checkNode {
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])') nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
"echo" "-e" "Node Version: ""$nodeVersion" "echo" "-e" "Node Version: ""$nodeVersion"
_0="12" _0="12"
if [ $(($nodeVersion < $_0)) == 1 ]; then if [ $(($nodeVersion <= $_0)) == 1 ]; then
"echo" "-e" "Error: Required Node.js 14" "echo" "-e" "Error: Required Node.js 14"
"exit" "1" "exit" "1"
fi
if [ "$nodeVersion" == "12" ]; then
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
fi fi
} }
function deb { function deb {
@ -50,8 +47,8 @@ fi
"echo" "-e" "Installing Curl" "echo" "-e" "Installing Curl"
apt --yes install curl apt --yes install curl
fi fi
"echo" "-e" "Installing Node.js 14" "echo" "-e" "Installing Node.js 16"
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
apt --yes install nodejs apt --yes install nodejs
node -v node -v
nodeCheckAgain=$(node -v) nodeCheckAgain=$(node -v)
@ -76,6 +73,9 @@ if [ "$type" == "local" ]; then
os=$(head -n1 /etc/issue | cut -f 1 -d ' ') os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
if [ "$os" == "Ubuntu" ]; then if [ "$os" == "Ubuntu" ]; then
distribution="ubuntu" distribution="ubuntu"
# Get ubuntu version
. /etc/lsb-release
version="$DISTRIB_RELEASE"
fi fi
if [ "$os" == "Debian" ]; then if [ "$os" == "Debian" ]; then
distribution="debian" distribution="debian"
@ -85,6 +85,7 @@ fi
arch=$(uname -i) arch=$(uname -i)
"echo" "-e" "Your OS: ""$os" "echo" "-e" "Your OS: ""$os"
"echo" "-e" "Distribution: ""$distribution" "echo" "-e" "Distribution: ""$distribution"
"echo" "-e" "Version: ""$version"
"echo" "-e" "Arch: ""$arch" "echo" "-e" "Arch: ""$arch"
if [ "$3" != "" ]; then if [ "$3" != "" ]; then
port="$3" port="$3"
@ -108,14 +109,27 @@ fi
if [ "$nodeCheck" != "" ]; then if [ "$nodeCheck" != "" ]; then
"checkNode" "checkNode"
else else
curlCheck=$(curl --version) dnfCheck=$(dnf --version)
if [ "$curlCheck" == "" ]; then # Use yum
"echo" "-e" "Installing Curl" if [ "$dnfCheck" == "" ]; then
yum -y -q install curl curlCheck=$(curl --version)
fi if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Node.js 14" "echo" "-e" "Installing Curl"
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt yum -y -q install curl
yum install -y -q nodejs fi
"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 node -v
nodeCheckAgain=$(node -v) nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then if [ "$nodeCheckAgain" == "" ]; then
@ -161,6 +175,12 @@ fi
"echo" "-e" "Installing PM2" "echo" "-e" "Installing PM2"
npm install pm2 -g && pm2 install pm2-logrotate npm install pm2 -g && pm2 install pm2-logrotate
pm2 startup pm2 startup
fi
# Check again
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Error: pm2 is not found!"
exit 1
fi fi
mkdir -p $installPath mkdir -p $installPath
cd $installPath cd $installPath

@ -1,13 +1,13 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.22.0", "version": "1.22.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/louislam/uptime-kuma.git" "url": "https://github.com/louislam/uptime-kuma.git"
}, },
"engines": { "engines": {
"node": "14.* || 16.* || 18.*" "node": "14 || 16 || 18 || >= 20.4.0"
}, },
"scripts": { "scripts": {
"install-legacy": "npm install", "install-legacy": "npm install",
@ -19,6 +19,7 @@
"lint": "npm run lint:js && npm run lint:style", "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\"", "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-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": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js", "start-server-dev": "cross-env NODE_ENV=development node server/server.js",
@ -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-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-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-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-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", "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", "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", "download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js", "reset-password": "node extra/reset-password.js",
"remove-2fa": "node extra/remove-2fa.js", "remove-2fa": "node extra/remove-2fa.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1", "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-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-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 .", "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-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-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", "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-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/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-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", "git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev", "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", "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
@ -67,7 +72,7 @@
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@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", "@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.27.0", "axios": "~0.27.0",
@ -95,10 +100,13 @@
"https-proxy-agent": "~5.0.1", "https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3", "iconv-lite": "~0.6.3",
"jsesc": "~3.0.2", "jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0", "jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"kafkajs": "^2.2.4",
"knex": "^2.4.2", "knex": "^2.4.2",
"limiter": "~2.1.0", "limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0", "mongodb": "~4.14.0",
"mqtt": "~4.3.7", "mqtt": "~4.3.7",
"mssql": "~8.1.4", "mssql": "~8.1.4",
@ -114,10 +122,11 @@
"playwright-core": "~1.35.1", "playwright-core": "~1.35.1",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1", "protobufjs": "~7.2.4",
"qs": "~6.10.4", "qs": "~6.10.4",
"redbean-node": "~0.3.0", "redbean-node": "~0.3.0",
"redis": "~4.5.1", "redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.6.1", "socket.io": "~4.6.1",
"socket.io-client": "~4.6.1", "socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1", "socks-proxy-agent": "6.1.1",
@ -127,7 +136,7 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "~5.0.1", "@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.17.0", "@babel/eslint-parser": "^7.22.7",
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36", "@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4", "@fortawesome/free-regular-svg-icons": "~5.15.4",
@ -135,9 +144,9 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5", "@fortawesome/vue-fontawesome": "~3.0.0-5",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~2.1.0", "@vitejs/plugin-legacy": "~4.1.0",
"@vitejs/plugin-vue": "~3.1.0", "@vitejs/plugin-vue": "~4.2.3",
"@vue/compiler-sfc": "~3.2.36", "@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8", "@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
@ -148,16 +157,16 @@
"core-js": "~3.26.1", "core-js": "~3.26.1",
"cronstrue": "~2.24.0", "cronstrue": "~2.24.0",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"cypress": "^10.1.0", "cypress": "^12.17.0",
"delay": "^5.0.0", "delay": "^5.0.0",
"dns2": "~2.0.1", "dns2": "~2.0.1",
"dompurify": "~2.4.3", "dompurify": "~2.4.3",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1", "eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10", "favico.js": "~0.3.10",
"jest": "~27.2.5", "jest": "~29.6.1",
"marked": "~4.2.5", "marked": "~4.2.5",
"node-ssh": "~13.0.1", "node-ssh": "~13.1.0",
"postcss-html": "~1.5.0", "postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2", "postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4", "postcss-scss": "~4.0.4",
@ -165,15 +174,16 @@
"qrcode": "~1.5.0", "qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1", "sass": "~1.42.1",
"stylelint": "~15.9.0", "stylelint": "^15.10.1",
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0", "terser": "~5.15.0",
"timezones-list": "~3.0.1", "timezones-list": "~3.0.1",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"v-pagination-3": "~0.1.7", "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", "vite-plugin-compression": "^0.5.1",
"vue": "~3.2.47", "vue": "~3.3.4",
"vue-chartjs": "~5.2.0", "vue-chartjs": "~5.2.0",
"vue-confirm-dialog": "~1.0.2", "vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4", "vue-contenteditable": "~3.0.4",

@ -1,27 +1,33 @@
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const axios = require("axios"); const axios = require("axios");
const compareVersions = require("compare-versions"); const compareVersions = require("compare-versions");
const { log } = require("../src/util");
exports.version = require("../package.json").version; exports.version = require("../package.json").version;
exports.latestVersion = null; 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; let interval;
/** Start 48 hour check interval */
exports.startInterval = () => { exports.startInterval = () => {
let check = async () => { let check = async () => {
if (await setting("checkUpdate") === false) {
return;
}
log.debug("update-checker", "Retrieving latest versions");
try { try {
const res = await axios.get("https://uptime.kuma.pet/version"); const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
// For debug // For debug
if (process.env.TEST_CHECK_VERSION === "1") { if (process.env.TEST_CHECK_VERSION === "1") {
res.data.slow = "1000.0.0"; res.data.slow = "1000.0.0";
} }
if (await setting("checkUpdate") === false) {
return;
}
let checkBeta = await setting("checkBeta"); let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) { if (checkBeta && res.data.beta) {
@ -35,12 +41,14 @@ exports.startInterval = () => {
exports.latestVersion = res.data.slow; exports.latestVersion = res.data.slow;
} }
} catch (_) { } } catch (_) {
log.info("update-checker", "Failed to check for new versions");
}
}; };
check(); 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. * Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
* @param {boolean} hideVersion
* @returns {Promise<void>} * @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", { socket.emit("info", {
version: checkVersion.version, version,
latestVersion: checkVersion.latestVersion, latestVersion,
primaryBaseURL: await setting("primaryBaseURL"), primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(), serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(), 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 demoMode = args["demo"] || false;
const badgeConstants = { const badgeConstants = {

@ -3,7 +3,6 @@ const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util"); const { log, sleep } = require("../src/util");
const knex = require("knex"); const knex = require("knex");
const { PluginsManager } = require("./plugins-manager");
const path = require("path"); const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb"); const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise"); const mysql = require("mysql2/promise");
@ -77,6 +76,9 @@ class Database {
"patch-monitor-tls.sql": true, "patch-monitor-tls.sql": true,
"patch-maintenance-cron.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-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 "/") // Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; 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"; Database.sqlitePath = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) { if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true }); 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 { UptimeKumaServer } = require("./uptime-kuma-server");
const { clearOldData } = require("./jobs/clear-old-data"); const { clearOldData } = require("./jobs/clear-old-data");
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
const Cron = require("croner"); const Cron = require("croner");
const jobs = [ const jobs = [
@ -9,6 +10,12 @@ const jobs = [
jobFunc: clearOldData, jobFunc: clearOldData,
croner: null, croner: null,
}, },
{
name: "incremental-vacuum",
interval: "*/5 * * * *",
jobFunc: incrementalVacuum,
croner: null,
}
]; ];
/** /**

@ -42,6 +42,8 @@ const clearOldData = async () => {
"DELETE FROM heartbeat WHERE time < " + sqlHourOffset, "DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
[ parsedPeriod * -24 ] [ parsedPeriod * -24 ]
); );
await R.exec("PRAGMA optimize;");
} catch (e) { } catch (e) {
log.error("clearOldData", `Failed to clear old data: ${e.message}`); log.error("clearOldData", `Failed to clear old data: ${e.message}`);
} }

@ -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 SQL_DATETIME_FORMAT
} = require("../../src/util"); } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery, const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, redisPingAsync, mongodbPing, kafkaProducerAsync
} = require("../util-server"); } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
@ -20,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker"); const { DockerHost } = require("../docker");
const { UptimeCacheList } = require("../uptime-cache-list"); const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig"); const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const Database = require("../database"); const Database = require("../database");
@ -98,6 +99,7 @@ class Monitor extends BeanModel {
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
resendInterval: this.resendInterval, resendInterval: this.resendInterval,
keyword: this.keyword, keyword: this.keyword,
invertKeyword: this.isInvertKeyword(),
expiryNotification: this.isEnabledExpiryNotification(), expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
@ -126,6 +128,13 @@ class Monitor extends BeanModel {
radiusCallingStationId: this.radiusCallingStationId, radiusCallingStationId: this.radiusCallingStationId,
game: this.game, game: this.game,
httpBodyEncoding: this.httpBodyEncoding, 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, screenshot,
}; };
@ -150,6 +159,7 @@ class Monitor extends BeanModel {
tlsCa: this.tlsCa, tlsCa: this.tlsCa,
tlsCert: this.tlsCert, tlsCert: this.tlsCert,
tlsKey: this.tlsKey, tlsKey: this.tlsKey,
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
}; };
} }
@ -164,7 +174,7 @@ class Monitor extends BeanModel {
async isActive() { async isActive() {
const parentActive = await Monitor.isParentActive(this.id); 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); return Boolean(this.upsideDown);
} }
/**
* Parse to boolean
* @returns {boolean}
*/
isInvertKeyword() {
return Boolean(this.invertKeyword);
}
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean}
@ -312,7 +330,7 @@ class Monitor extends BeanModel {
bean.msg = "Group empty"; 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" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
@ -440,7 +458,7 @@ class Monitor extends BeanModel {
if (this.type === "http") { if (this.type === "http") {
bean.status = UP; bean.status = UP;
} else { } else if (this.type === "keyword") {
let data = res.data; let data = res.data;
@ -449,17 +467,37 @@ class Monitor extends BeanModel {
data = JSON.stringify(data); data = JSON.stringify(data);
} }
if (data.includes(this.keyword)) { let keywordFound = data.includes(this.keyword);
bean.msg += ", keyword is found"; if (keywordFound === !this.isInvertKeyword()) {
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
bean.status = UP; bean.status = UP;
} else { } else {
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim(); data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
if (data.length > 50) { if (data.length > 50) {
data = data.substring(0, 47) + "..."; 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") { } 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 // No need to insert successful heartbeat for push type, so end here
retries = 0; retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`); log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout); this.heartbeatInterval = setTimeout(safeBeat, timeout);
return; return;
} }
} else { } else {
@ -627,9 +665,15 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Request`); log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options); let res = await axios.request(options);
if (res.data.State.Running) { if (res.data.State.Running) {
bean.status = UP; if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
bean.msg = res.data.State.Status; 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 { } else {
throw Error("Container State is " + res.data.State.Status); throw Error("Container State is " + res.data.State.Status);
} }
@ -658,7 +702,6 @@ class Monitor extends BeanModel {
grpcEnableTls: this.grpcEnableTls, grpcEnableTls: this.grpcEnableTls,
grpcMethod: this.grpcMethod, grpcMethod: this.grpcMethod,
grpcBody: this.grpcBody, grpcBody: this.grpcBody,
keyword: this.keyword
}; };
const response = await grpcQuery(options); const response = await grpcQuery(options);
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@ -671,13 +714,14 @@ class Monitor extends BeanModel {
bean.status = DOWN; bean.status = DOWN;
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`; bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
} else { } else {
if (response.data.toString().includes(this.keyword)) { let keywordFound = response.data.toString().includes(this.keyword);
if (keywordFound === !this.isInvertKeyword()) {
bean.status = UP; bean.status = UP;
bean.msg = `${responseData}, keyword [${this.keyword}] is found`; bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
} else { } 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.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") { } else if (this.type === "postgres") {
@ -724,7 +768,8 @@ class Monitor extends BeanModel {
this.radiusCalledStationId, this.radiusCalledStationId,
this.radiusCallingStationId, this.radiusCallingStationId,
this.radiusSecret, this.radiusSecret,
port port,
this.interval * 1000 * 0.8,
); );
if (resp.code) { if (resp.code) {
bean.msg = resp.code; bean.msg = resp.code;
@ -754,6 +799,24 @@ class Monitor extends BeanModel {
bean.ping = dayjs().valueOf() - startTime; 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 { } else {
throw new Error("Unknown Monitor Type"); throw new Error("Unknown Monitor Type");
} }

@ -1,5 +1,5 @@
const { MonitorType } = require("./monitor-type"); const { MonitorType } = require("./monitor-type");
const { chromium, Browser } = require("playwright-core"); const { chromium } = require("playwright-core");
const { UP, log } = require("../../src/util"); const { UP, log } = require("../../src/util");
const { Settings } = require("../settings"); const { Settings } = require("../settings");
const commandExistsSync = require("command-exists").sync; const commandExistsSync = require("command-exists").sync;
@ -7,13 +7,60 @@ const childProcess = require("child_process");
const path = require("path"); const path = require("path");
const Database = require("../database"); const Database = require("../database");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const config = require("../config");
/**
*
* @type {Browser}
*/
let browser = null; 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() { async function getBrowser() {
if (!browser) { if (!browser) {
let executablePath = await Settings.get("chromeExecutable"); let executablePath = await Settings.get("chromeExecutable");
@ -31,6 +78,7 @@ async function getBrowser() {
async function prepareChromeExecutable(executablePath) { async function prepareChromeExecutable(executablePath) {
// Special code for using the playwright_chromium // Special code for using the playwright_chromium
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") { if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
// Set to undefined = use playwright_chromium
executablePath = undefined; executablePath = undefined;
} else if (!executablePath) { } else if (!executablePath) {
if (process.env.UPTIME_KUMA_IS_CONTAINER) { if (process.env.UPTIME_KUMA_IS_CONTAINER) {
@ -60,30 +108,30 @@ async function prepareChromeExecutable(executablePath) {
}); });
} }
} else if (process.platform === "win32") { } else {
executablePath = findChrome([ executablePath = findChrome(allowedList);
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", }
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", } else {
"D:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", // User specified a path
"D:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", // Check if the executablePath is in the list of allowed
"E:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", if (!await isAllowedChromeExecutable(executablePath)) {
"E:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", 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.");
]);
} else if (process.platform === "linux") {
executablePath = findChrome([
"chromium-browser",
"chromium",
"google-chrome",
]);
} }
// TODO: Mac??
} }
return executablePath; return executablePath;
} }
function findChrome(executables) { 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) { for (let executable of executables) {
if (commandExistsSync(executable)) { if (commandExistsSync(executable)) {
lastAutoDetectChromeExecutable = executable;
return executable; return executable;
} }
} }

@ -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) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
if (notification.slackchannelnotify) {
msg += " <!channel>";
}
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let data = {
@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
"type": "header", "type": "header",
"text": { "text": {
"type": "plain_text", "type": "plain_text",
"text": "Uptime Kuma Alert", "text": textMsg,
}, },
}, },
{ {

@ -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, port: notification.smtpPort,
secure: notification.smtpSecure, secure: notification.smtpSecure,
tls: { tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false, rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
} }
}; };
@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) { if (monitorJSON !== null) {
monitorName = monitorJSON["name"]; monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") { if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"]; monitorHostnameOrURL = monitorJSON["url"];
} else { } else {
monitorHostnameOrURL = monitorJSON["hostname"]; monitorHostnameOrURL = monitorJSON["hostname"];

@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
let accountSID = notification.twilioAccountSID; let accountSID = notification.twilioAccountSID;
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
let authToken = notification.twilioAuthToken; let authToken = notification.twilioAuthToken;
try { try {
@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
let config = { let config = {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8", "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 NotificationProvider = require("./notification-provider");
const axios = require("axios"); const axios = require("axios");
const FormData = require("form-data"); const FormData = require("form-data");
const { Liquid } = require("liquidjs");
class Webhook extends NotificationProvider { class Webhook extends NotificationProvider {
@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
monitor: monitorJSON, monitor: monitorJSON,
msg, msg,
}; };
let finalData;
let config = { let config = {
headers: {} headers: {}
}; };
if (notification.webhookContentType === "form-data") { if (notification.webhookContentType === "form-data") {
finalData = new FormData(); const formData = new FormData();
finalData.append("data", JSON.stringify(data)); formData.append("data", JSON.stringify(data));
config.headers = finalData.getHeaders(); config.headers = formData.getHeaders();
} else { data = formData;
finalData = data; } 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) { 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; return okMsg;
} catch (error) { } catch (error) {

@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark"); const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms"); const ClickSendSMS = require("./notification-providers/clicksendsms");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding"); const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu"); const Feishu = require("./notification-providers/feishu");
@ -68,6 +69,7 @@ class Notification {
new Apprise(), new Apprise(),
new Bark(), new Bark(),
new ClickSendSMS(), new ClickSendSMS(),
new SMSC(),
new DingDing(), new DingDing(),
new Discord(), new Discord(),
new Feishu(), 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) { if (!tlsInfo.valid) {
// return a "Bad Cert" badge in naColor (grey), when cert is not valid // return a "Bad Cert" badge in naColor (grey), when cert is not valid
badgeValues.message = "Bad Cert"; badgeValues.message = "Bad Cert";
badgeValues.color = badgeConstants.downColor; badgeValues.color = downColor;
} else { } else {
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining); const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);

@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, sendHttpError } = require("../util-server"); const { allowDevAllOrigin, sendHttpError } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const { badgeConstants } = require("../config");
const { makeBadge } = require("badge-maker");
let router = express.Router(); 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; module.exports = router;

@ -15,20 +15,27 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
require("dotenv").config(); require("dotenv").config();
// Check Node.js Version // Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]); const nodeVersion = process.versions.node;
const requiredVersion = 14;
// 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}`); console.log(`Your Node.js version: ${nodeVersion}`);
// See more: https://github.com/louislam/uptime-kuma/issues/3138 const semver = require("semver");
if (nodeVersion >= 20) { const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
}
if (nodeVersion < requiredVersion) { // Exit Uptime Kuma immediately if the Node.js version is banned
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`); 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); 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); const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util"); const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config"); const config = require("./config");
@ -42,6 +49,7 @@ if (! process.env.NODE_ENV) {
} }
log.info("server", "Node Env: " + 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"); log.info("server", "Importing Node libraries");
const fs = require("fs"); 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 { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
const apicache = require("./modules/apicache"); const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type"); const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
const { EmbeddedMariaDB } = require("./embedded-mariadb"); const { EmbeddedMariaDB } = require("./embedded-mariadb");
@ -193,7 +200,6 @@ let needSetup = false;
// Database should be ready now // Database should be ready now
await server.initAfterDatabaseReady(); await server.initAfterDatabaseReady();
server.loadPlugins();
server.entryPage = await Settings.get("entryPage"); server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList(); await StatusPage.loadDomainMappingList();
@ -239,6 +245,7 @@ let needSetup = false;
}); });
if (isDev) { if (isDev) {
app.use(express.urlencoded({ extended: true }));
app.post("/test-webhook", async (request, response) => { app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers); log.debug("test", request.headers);
log.debug("test", request.body); log.debug("test", request.body);
@ -293,7 +300,7 @@ let needSetup = false;
log.info("server", "Adding socket handler"); log.info("server", "Adding socket handler");
io.on("connection", async (socket) => { io.on("connection", async (socket) => {
sendInfo(socket); sendInfo(socket, true);
if (needSetup) { if (needSetup) {
log.info("server", "Redirect to setup page"); log.info("server", "Redirect to setup page");
@ -666,6 +673,9 @@ let needSetup = false;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes; delete monitor.accepted_statuscodes;
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.import(monitor); bean.import(monitor);
bean.user_id = socket.userID; bean.user_id = socket.userID;
@ -748,6 +758,7 @@ let needSetup = false;
} }
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.invertKeyword = monitor.invertKeyword;
bean.ignoreTls = monitor.ignoreTls; bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification; bean.expiryNotification = monitor.expiryNotification;
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
@ -782,6 +793,13 @@ let needSetup = false;
bean.radiusCallingStationId = monitor.radiusCallingStationId; bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret; bean.radiusSecret = monitor.radiusSecret;
bean.httpBodyEncoding = monitor.httpBodyEncoding; 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(); bean.validate();
@ -1415,6 +1433,7 @@ let needSetup = false;
maxretries: monitorListData[i].maxretries, maxretries: monitorListData[i].maxretries,
port: monitorListData[i].port, port: monitorListData[i].port,
keyword: monitorListData[i].keyword, keyword: monitorListData[i].keyword,
invertKeyword: monitorListData[i].invertKeyword,
ignoreTls: monitorListData[i].ignoreTls, ignoreTls: monitorListData[i].ignoreTls,
upsideDown: monitorListData[i].upsideDown, upsideDown: monitorListData[i].upsideDown,
maxredirects: monitorListData[i].maxredirects, maxredirects: monitorListData[i].maxredirects,
@ -1583,7 +1602,6 @@ let needSetup = false;
maintenanceSocketHandler(socket); maintenanceSocketHandler(socket);
apiKeySocketHandler(socket); apiKeySocketHandler(socket);
generalSocketHandler(socket, server); generalSocketHandler(socket, server);
pluginsHandler(socket, server);
log.debug("server", "added all socket handlers"); log.debug("server", "added all socket handlers");
@ -1609,6 +1627,8 @@ let needSetup = false;
await shutdownFunction(); await shutdownFunction();
}); });
server.start();
server.httpServer.listen(port, hostname, () => { server.httpServer.listen(port, hostname, () => {
if (hostname) { if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`); log.info("server", `Listening on ${hostname}:${port}`);
@ -1686,6 +1706,7 @@ async function afterLogin(socket, user) {
socket.join(user.id); socket.join(user.id);
let monitorList = await server.sendMonitorList(socket); let monitorList = await server.sendMonitorList(socket);
sendInfo(socket);
server.sendMaintenanceList(socket); server.sendMaintenanceList(socket);
sendNotificationList(socket); sendNotificationList(socket);
sendProxyList(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 { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { PluginsManager } = require("./plugins-manager"); const childProcess = require("child_process");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()` // DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/** /**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -47,12 +47,6 @@ class UptimeKumaServer {
*/ */
indexHTML = ""; indexHTML = "";
/**
* Plugins Manager
* @type {PluginsManager}
*/
pluginsManager = null;
/** /**
* *
* @type {{}} * @type {{}}
@ -106,6 +100,7 @@ class UptimeKumaServer {
// Set Monitor Types // Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }
@ -256,9 +251,9 @@ class UptimeKumaServer {
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null) return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"] || socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, ""); || clientIP.replace(/^::ffff:/, "");
} else { } else {
return clientIP.replace(/^.*:/, ""); return clientIP.replace(/^::ffff:/, "");
} }
} }
@ -269,13 +264,43 @@ class UptimeKumaServer {
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async getTimezone() { 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"); let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone; // From Settings
} else if (process.env.TZ) { try {
return process.env.TZ; log.debug("timezone", "Using timezone from settings: " + timezone);
} else { if (timezone) {
return dayjs.tz.guess(); 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"); 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 * Set the current server timezone and environment variables
* @param {string} timezone * @param {string} timezone
*/ */
async setTimezone(timezone) { async setTimezone(timezone) {
this.checkTimezone(timezone);
await Settings.set("serverTimezone", timezone, "general"); await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone; process.env.TZ = timezone;
dayjs.tz.setDefault(timezone); dayjs.tz.setDefault(timezone);
} }
/** Stop the server */ /**
async stop() { * TODO: Listen logic should be moved to here
* @returns {Promise<void>}
} */
async start() {
loadPlugins() { this.startServices();
this.pluginsManager = new PluginsManager(this);
} }
/** /**
* * Stop the server
* @returns {PluginsManager} * @returns {Promise<void>}
*/ */
getPluginManager() { async stop() {
return this.pluginsManager; this.stopServices();
} }
/** /**
* * Start all system services (e.g. nscd)
* @param {MonitorType} monitorType * For now, only used in Docker
*/ */
addMonitorType(monitorType) { startServices() {
if (monitorType instanceof MonitorType && monitorType.name) { if (process.env.UPTIME_KUMA_IS_CONTAINER) {
if (monitorType.name in UptimeKumaServer.monitorTypeList) { try {
log.error("", "Conflict Monitor Type name"); 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);
} }
} }
/** /**
* * Stop all system services
* @param {MonitorType} monitorType
*/ */
removeMonitorType(monitorType) { stopServices() {
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) { if (process.env.UPTIME_KUMA_IS_CONTAINER) {
delete UptimeKumaServer.monitorTypeList[monitorType.name]; try {
} else { log.info("services", "Stopping nscd");
log.error("", "Remove MonitorType failed: " + monitorType.name); childProcess.execSync("sudo service nscd stop");
} catch (e) {
log.info("services", "Failed to stop nscd");
}
} }
} }
} }
module.exports = { module.exports = {
UptimeKumaServer UptimeKumaServer
}; };
// Must be at the end // Must be at the end to avoid circular dependencies
const { MonitorType } = require("./monitor-types/monitor-type");
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type"); 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, const rl = readline.createInterface({ input: process.stdin,
output: process.stdout }); 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 * Init or reset JWT secret
* @returns {Promise<Bean>} * @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. * Use NTLM Auth for a http request.
* @param {Object} options The http request options * @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} callingStationId ID of calling station
* @param {string} secret Secret to use * @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on * @param {number} [port=1812] Port to contact radius server on
* @param {number} [timeout=2500] Timeout for connection to use
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
exports.radius = function ( exports.radius = function (
@ -391,10 +483,12 @@ exports.radius = function (
callingStationId, callingStationId,
secret, secret,
port = 1812, port = 1812,
timeout = 2500,
) { ) {
const client = new radiusClient({ const client = new radiusClient({
host: hostname, host: hostname,
hostPort: port, hostPort: port,
timeout: timeout,
dictionaries: [ file ], dictionaries: [ file ],
}); });

@ -436,12 +436,12 @@ optgroup {
.monitor-list { .monitor-list {
&.scrollbar { &.scrollbar {
overflow-y: auto; overflow-y: auto;
height: calc(100% - 65px); height: calc(100% - 107px);
} }
@media (max-width: 770px) { @media (max-width: 770px) {
&.scrollbar { &.scrollbar {
height: calc(100% - 40px); height: calc(100% - 97px);
} }
} }

@ -69,6 +69,7 @@
.multiselect__content-wrapper { .multiselect__content-wrapper {
background-color: $dark-bg2; background-color: $dark-bg2;
border-color: $dark-border-color; border-color: $dark-border-color;
z-index: 150;
} }
.multiselect--above .multiselect__content-wrapper { .multiselect--above .multiselect__content-wrapper {

@ -22,78 +22,78 @@
</div> </div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
<label for="duration" class="form-label">{{ $t("Badge Duration") }}</label> <label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
<input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required> <input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
</div> </div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
<label for="label" class="form-label">{{ $t("Badge Label") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
<label for="color" class="form-label">{{ $t("Badge Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label> <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>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3"> <div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label> <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>
<div class="mb-3"> <div class="mb-3">
@ -109,12 +109,16 @@
<div class="mb-3"> <div class="mb-3">
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label> <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>
<div class="my-3"> <div class="my-3">
<label for="push-url" class="form-label">{{ $t("Badge URL") }}</label> <label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
<CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" /> <CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
</div> </div>
</div> </div>
@ -131,6 +135,7 @@
<script lang="ts"> <script lang="ts">
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import CopyableInput from "./CopyableInput.vue"; import CopyableInput from "./CopyableInput.vue";
import { default as serverConfig } from "../../server/config.js";
export default { export default {
components: { components: {
@ -224,7 +229,8 @@ export default {
"color", "color",
"labelColor", "labelColor",
], ],
} },
badgeConstants: serverConfig.badgeConstants,
}; };
}, },

@ -1,17 +1,25 @@
<template> <template>
<div class="shadow-box mb-3" :style="boxStyle"> <div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header"> <div class="list-header">
<div class="placeholder"></div> <div class="header-top">
<div class="search-wrapper"> <div class="placeholder"></div>
<a v-if="searchText == ''" class="search-icon"> <div class="search-wrapper">
<font-awesome-icon icon="search" /> <a v-if="searchText == ''" class="search-icon">
</a> <font-awesome-icon icon="search" />
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText"> </a>
<font-awesome-icon icon="times" /> <a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
</a> <font-awesome-icon icon="times" />
<form> </a>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" /> <form>
</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> </div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }"> <div class="monitor-list" :class="{ scrollbar: scrollbar }">
@ -19,18 +27,23 @@
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div> </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>
</div> </div>
</template> </template>
<script> <script>
import MonitorListItem from "../components/MonitorListItem.vue"; import MonitorListItem from "../components/MonitorListItem.vue";
import MonitorListFilter from "./MonitorListFilter.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
export default { export default {
components: { components: {
MonitorListItem, MonitorListItem,
MonitorListFilter,
}, },
props: { props: {
/** Should the scrollbar be shown */ /** Should the scrollbar be shown */
@ -42,6 +55,11 @@ export default {
return { return {
searchText: "", searchText: "",
windowTop: 0, windowTop: 0,
filterState: {
status: null,
active: null,
tags: null,
}
}; };
}, },
computed: { computed: {
@ -72,8 +90,8 @@ export default {
const loweredSearchText = this.searchText.toLowerCase(); const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => { result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText) return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText)); || tag.value?.toLowerCase().includes(loweredSearchText));
}); });
} else { } else {
result = result.filter(monitor => monitor.parent === null); result = result.filter(monitor => monitor.parent === null);
@ -105,6 +123,27 @@ export default {
return m1.name.localeCompare(m2.name); 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; return result;
}, },
}, },
@ -134,7 +173,14 @@ export default {
/** Clear the search bar */ /** Clear the search bar */
clearSearchText() { clearSearchText() {
this.searchText = ""; this.searchText = "";
} },
/**
* Update the MonitorList Filter
* @param {object} newFilter Object with new filter
*/
updateFilter(newFilter) {
this.filterState = newFilter;
},
}, },
}; };
</script> </script>
@ -159,8 +205,6 @@ export default {
margin: -10px; margin: -10px;
margin-bottom: 10px; margin-bottom: 10px;
padding: 10px; padding: 10px;
display: flex;
justify-content: space-between;
.dark & { .dark & {
background-color: $dark-header-bg; 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) { @media (max-width: 770px) {
.list-header { .list-header {
margin: -20px; margin: -20px;
@ -216,5 +271,4 @@ export default {
padding-left: 67px; padding-left: 67px;
margin-top: 5px; margin-top: 5px;
} }
</style> </style>

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

@ -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 // We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet // prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { 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; return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
}, },

@ -164,6 +164,7 @@ export default {
"SMSManager": "SmsManager (smsmanager.cz)", "SMSManager": "SmsManager (smsmanager.cz)",
"WeCom": "WeCom (企业微信群机器人)", "WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)", "ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
}; };
// Sort by notification name // 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 // We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet // prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { 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; return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
}, },

@ -99,7 +99,7 @@
</div> </div>
<div class="modal-footer"> <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") }} {{ $t("Delete") }}
</button> </button>
<button type="submit" class="btn btn-primary" :disabled="processing"> <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> <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"> <select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
<option value="ios">iOS</option> <option value="ios">iOS</option>
<option value="android">{{ $t("Android") }}</option> <option value="android">Android</option>
<option value="huawei">{{ $t("Huawei") }}</option> <option value="huawei">{{ $t("Huawei") }}</option>
</select> </select>
</div> </div>

@ -13,7 +13,7 @@
<div class="form-text"> <div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }} <span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;"> <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> </i18n-t>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
{{ $t("aboutMattermostChannelName") }} {{ $t("aboutMattermostChannelName") }}

@ -7,8 +7,9 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ntfy-server-url" class="form-label">{{ $t("Server URL") }}</label> <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> </div>
<div class="mb-3"> <div class="mb-3">

@ -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> <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</i18n-t> </i18n-t>
</div> </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> </div>
</template> </template>

@ -5,7 +5,18 @@
</div> </div>
<div class="mb-3"> <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> <input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
</div> </div>

@ -12,61 +12,97 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="webhook-content-type" class="form-label">{{ <label for="webhook-request-body" class="form-label">{{
$t("Content Type") $t("Request Body")
}}</label> }}</label>
<select <select
id="webhook-content-type" id="webhook-request-body"
v-model="$parent.notification.webhookContentType" v-model="$parent.notification.webhookContentType"
class="form-select" class="form-select"
required required
> >
<option value="json">application/json</option> <option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
<option value="form-data">multipart/form-data</option> <option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
</select> </select>
<div class="form-text"> <div class="form-text">
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p> <div v-if="$parent.notification.webhookContentType == 'json'">
<i18n-t tag="p" keypath="webhookFormDataDesc"> <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
<template #multipart>"multipart/form-data"</template> </div>
<template #decodeFunction> <div v-if="$parent.notification.webhookContentType == 'form-data'">
<strong>json_decode($_POST['data'])</strong> <i18n-t tag="p" keypath="webhookFormDataDesc">
</template> <template #multipart>multipart/form-data"</template>
</i18n-t> <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> </div>
<textarea
v-if="$parent.notification.webhookContentType == 'custom'"
id="customBody"
v-model="$parent.notification.webhookCustomBody"
class="form-control"
:placeholder="customBodyPlaceholder"
></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<i18n-t <div class="form-check form-switch">
tag="label" <input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
class="form-label" <label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
for="additionalHeaders" </div>
keypath="webhookAdditionalHeadersTitle" <div class="form-text">
> <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</i18n-t> </div>
<textarea <textarea
v-if="showAdditionalHeadersField"
id="additionalHeaders" id="additionalHeaders"
v-model="$parent.notification.webhookAdditionalHeaders" v-model="$parent.notification.webhookAdditionalHeaders"
class="form-control" class="form-control"
:placeholder="headersPlaceholder" :placeholder="headersPlaceholder"
></textarea> ></textarea>
<div class="form-text">
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() {
return {
showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
};
},
computed: { computed: {
headersPlaceholder() { headersPlaceholder() {
return this.$t("Example:", [ return this.$t("Example:", [
` `
{ {
"HeaderName": "HeaderValue" "Authorization": "Authorization Token"
}`, }`,
]); ]);
}, },
customBodyPlaceholder() {
return `Example:
{
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
"Body": "{{ msg }}"
}`;
}
}, },
}; };
</script> </script>

@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue"; import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue"; import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue"; import ClickSendSMS from "./ClickSendSMS.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue"; import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue"; import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue"; import Feishu from "./Feishu.vue";
@ -61,6 +62,7 @@ const NotificationFormList = {
"apprise": Apprise, "apprise": Apprise,
"Bark": Bark, "Bark": Bark,
"clicksendsms": ClickSendSMS, "clicksendsms": ClickSendSMS,
"smsc": SMSC,
"DingDing": DingDing, "DingDing": DingDing,
"discord": Discord, "discord": Discord,
"Feishu": Feishu, "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": "للسلامة يجب استخدام المفتاح السري", "For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
"Device Token": "رمز الجهاز", "Device Token": "رمز الجهاز",
"Platform": "منصة", "Platform": "منصة",
"iOS": "iOS",
"Android": "ذكري المظهر",
"Huawei": "هواوي", "Huawei": "هواوي",
"High": "عالٍ", "High": "عالٍ",
"Retry": "إعادة المحاولة", "Retry": "إعادة المحاولة",

@ -592,7 +592,6 @@
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري", "For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
"Device Token": "رمز الجهاز", "Device Token": "رمز الجهاز",
"Platform": "منصة", "Platform": "منصة",
"Android": "ذكري المظهر",
"Huawei": "هواوي", "Huawei": "هواوي",
"High": "عالٍ", "High": "عالٍ",
"Retry": "إعادة المحاولة", "Retry": "إعادة المحاولة",

@ -396,8 +396,6 @@
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ", "For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
"Device Token": "Токен за устройство", "Device Token": "Токен за устройство",
"Platform": "Платформа", "Platform": "Платформа",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Висок", "High": "Висок",
"Retry": "Повтори", "Retry": "Повтори",

@ -454,8 +454,6 @@
"For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key", "For safety, must use secret key": "Z důvodu bezpečnosti použijte secret key",
"Device Token": "Token zařízení", "Device Token": "Token zařízení",
"Platform": "Platforma", "Platform": "Platforma",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Vysoký", "High": "Vysoký",
"Retry": "Opakovat", "Retry": "Opakovat",

@ -558,7 +558,6 @@
"high": "høj", "high": "høj",
"Base URL": "Base URL", "Base URL": "Base URL",
"Platform": "Platform", "Platform": "Platform",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"Retry": "Forsøg igen", "Retry": "Forsøg igen",
"Topic": "Emne", "Topic": "Emne",

@ -403,8 +403,6 @@
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden", "For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
"Device Token": "Gerätetoken", "Device Token": "Gerätetoken",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Hoch", "High": "Hoch",
"Retry": "Wiederholungen", "Retry": "Wiederholungen",

@ -403,8 +403,6 @@
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden", "For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
"Device Token": "Gerätetoken", "Device Token": "Gerätetoken",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Hoch", "High": "Hoch",
"Retry": "Wiederholungen", "Retry": "Wiederholungen",

@ -420,8 +420,6 @@
"For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key", "For safety, must use secret key": "Για ασφάλεια, πρέπει να χρησιμοποιήσετε secret key",
"Device Token": "Device Token", "Device Token": "Device Token",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "Ξαναδοκιμάσετε", "Retry": "Ξαναδοκιμάσετε",

@ -56,6 +56,9 @@
"Ping": "Ping", "Ping": "Ping",
"Monitor Type": "Monitor Type", "Monitor Type": "Monitor Type",
"Keyword": "Keyword", "Keyword": "Keyword",
"Invert Keyword": "Invert Keyword",
"Expected Value": "Expected Value",
"Json Query": "Json Query",
"Friendly Name": "Friendly Name", "Friendly Name": "Friendly Name",
"URL": "URL", "URL": "URL",
"Hostname": "Hostname", "Hostname": "Hostname",
@ -157,6 +160,8 @@
"Disable 2FA": "Disable 2FA", "Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings", "2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication", "Two Factor Authentication": "Two Factor Authentication",
"filterActive": "Active",
"filterActivePaused": "Paused",
"Active": "Active", "Active": "Active",
"Inactive": "Inactive", "Inactive": "Inactive",
"Token": "Token", "Token": "Token",
@ -200,8 +205,11 @@
"Content Type": "Content Type", "Content Type": "Content Type",
"webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js", "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}", "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", "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", "Webhook URL": "Webhook URL",
"Application Token": "Application Token", "Application Token": "Application Token",
"Server URL": "Server URL", "Server URL": "Server URL",
@ -361,6 +369,7 @@
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?", "deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
"socket": "Socket", "socket": "Socket",
"tcp": "TCP / HTTP", "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", "Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID", "Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host", "Docker Host": "Docker Host",
@ -523,6 +532,8 @@
"passwordNotMatchMsg": "The repeat password does not match.", "passwordNotMatchMsg": "The repeat password does not match.",
"notificationDescription": "Notifications must be assigned to a monitor to function.", "notificationDescription": "Notifications must be assigned to a monitor to function.",
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.", "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.", "backupDescription": "You can backup all monitors and notifications into a JSON file.",
"backupDescription2": "Note: history and event data is not included.", "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.", "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", "For safety, must use secret key": "For safety, must use secret key",
"Device Token": "Device Token", "Device Token": "Device Token",
"Platform": "Platform", "Platform": "Platform",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "Retry", "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.", "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}", "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", "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", "Uptime Kuma URL": "Uptime Kuma URL",
"Icon Emoji": "Icon Emoji", "Icon Emoji": "Icon Emoji",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!", "signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
@ -683,6 +695,7 @@
"Octopush API Version": "Octopush API Version", "Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM", "Legacy Octopush-DM": "Legacy Octopush-DM",
"ntfy Topic": "ntfy Topic", "ntfy Topic": "ntfy Topic",
"Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic",
"onebotHttpAddress": "OneBot HTTP Address", "onebotHttpAddress": "OneBot HTTP Address",
"onebotMessageType": "OneBot Message Type", "onebotMessageType": "OneBot Message Type",
"onebotGroupMessage": "Group", "onebotGroupMessage": "Group",
@ -730,7 +743,8 @@
"ntfyAuthenticationMethod": "Authentication Method", "ntfyAuthenticationMethod": "Authentication Method",
"ntfyUsernameAndPassword": "Username and Password", "ntfyUsernameAndPassword": "Username and Password",
"twilioAccountSID": "Account SID", "twilioAccountSID": "Account SID",
"twilioAuthToken": "Auth Token", "twilioApiKey": "Api Key (optional)",
"twilioAuthToken": "Auth Token / Api Key Secret",
"twilioFromNumber": "From Number", "twilioFromNumber": "From Number",
"twilioToNumber": "To Number", "twilioToNumber": "To Number",
"Monitor Setting": "{0}'s Monitor Setting", "Monitor Setting": "{0}'s Monitor Setting",
@ -739,13 +753,14 @@
"Open Badge Generator": "Open Badge Generator", "Open Badge Generator": "Open Badge Generator",
"Badge Generator": "{0}'s Badge Generator", "Badge Generator": "{0}'s Badge Generator",
"Badge Type": "Badge Type", "Badge Type": "Badge Type",
"Badge Duration": "Badge Duration", "Badge Duration (in hours)": "Badge Duration (in hours)",
"Badge Label": "Badge Label", "Badge Label": "Badge Label",
"Badge Prefix": "Badge Prefix", "Badge Prefix": "Badge Value Prefix",
"Badge Suffix": "Badge Suffix", "Badge Suffix": "Badge Value Suffix",
"Badge Label Color": "Badge Label Color", "Badge Label Color": "Badge Label Color",
"Badge Color": "Badge Color", "Badge Color": "Badge Color",
"Badge Label Prefix": "Badge Label Prefix", "Badge Label Prefix": "Badge Label Prefix",
"Badge Preview": "Badge Preview",
"Badge Label Suffix": "Badge Label Suffix", "Badge Label Suffix": "Badge Label Suffix",
"Badge Up Color": "Badge Up Color", "Badge Up Color": "Badge Up Color",
"Badge Down Color": "Badge Down Color", "Badge Down Color": "Badge Down Color",
@ -759,6 +774,21 @@
"Badge URL": "Badge URL", "Badge URL": "Badge URL",
"Group": "Group", "Group": "Group",
"Monitor Group": "Monitor 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.", "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 Method": "Método Proto",
"Proto Content": "Contenido Proto", "Proto Content": "Contenido Proto",
"Economy": "Económico", "Economy": "Económico",
"iOS": "iOS",
"Android": "Android",
"Platform": "Plataforma", "Platform": "Plataforma",
"onebotPrivateMessage": "Privado", "onebotPrivateMessage": "Privado",
"onebotMessageType": "Tipo de Mensaje OneBot", "onebotMessageType": "Tipo de Mensaje OneBot",

@ -415,8 +415,6 @@
"For safety, must use secret key": "For safety, must use secret key", "For safety, must use secret key": "For safety, must use secret key",
"Device Token": "Gailu tokena", "Device Token": "Gailu tokena",
"Platform": "Plataforma", "Platform": "Plataforma",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Altua", "High": "Altua",
"Retry": "Errepikatu", "Retry": "Errepikatu",

@ -568,7 +568,6 @@
"SendKey": "کلید ارسال (SendKey)", "SendKey": "کلید ارسال (SendKey)",
"SecretAccessKey": "کلید دسترسی مخفی (AccessKey Secret)", "SecretAccessKey": "کلید دسترسی مخفی (AccessKey Secret)",
"SignName": "نام امضا (SignName)", "SignName": "نام امضا (SignName)",
"Android": "اندروید",
"Huawei": "هواوی", "Huawei": "هواوی",
"WeCom Bot Key": "کلید ربات WeCom", "WeCom Bot Key": "کلید ربات WeCom",
"Setup Proxy": "تنظیم پروکسی", "Setup Proxy": "تنظیم پروکسی",

@ -547,7 +547,6 @@
"For safety, must use secret key": "Turvallisuuden vuoksi on käytettävä salaista avainta", "For safety, must use secret key": "Turvallisuuden vuoksi on käytettävä salaista avainta",
"Device Token": "Laitteen tunnus", "Device Token": "Laitteen tunnus",
"Platform": "Alusta", "Platform": "Alusta",
"iOS": "iOS",
"Bark Endpoint": "Bark päätepiste", "Bark Endpoint": "Bark päätepiste",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Korkea", "High": "Korkea",
@ -564,7 +563,6 @@
"promosmsAllowLongSMS": "Salli pitkät tekstiviestit", "promosmsAllowLongSMS": "Salli pitkät tekstiviestit",
"Feishu WebHookUrl": "Feishu WebHookURL-osoite", "Feishu WebHookUrl": "Feishu WebHookURL-osoite",
"Internal Room Id": "Huoneen sisäinen tunnus", "Internal Room Id": "Huoneen sisäinen tunnus",
"Android": "Android",
"Channel Name": "Kanavan nimi", "Channel Name": "Kanavan nimi",
"Uptime Kuma URL": "Uptime Kuma URL-osoite", "Uptime Kuma URL": "Uptime Kuma URL-osoite",
"Icon Emoji": "Ikoni Emoji", "Icon Emoji": "Ikoni Emoji",

@ -451,8 +451,6 @@
"For safety, must use secret key": "Par sécurité, utilisation obligatoire de la clé secrète", "For safety, must use secret key": "Par sécurité, utilisation obligatoire de la clé secrète",
"Device Token": "Jeton d'appareil", "Device Token": "Jeton d'appareil",
"Platform": "Plateforme", "Platform": "Plateforme",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Haute", "High": "Haute",
"Retry": "Recommencez", "Retry": "Recommencez",

@ -445,8 +445,6 @@
"For safety, must use secret key": "לבטיחות, חייב להשתמש במפתח סודיy", "For safety, must use secret key": "לבטיחות, חייב להשתמש במפתח סודיy",
"Device Token": "אסימון מכשיר", "Device Token": "אסימון מכשיר",
"Platform": "פּלַטפוֹרמָה", "Platform": "פּלַטפוֹרמָה",
"iOS": "iOS",
"Android": "דְמוּי אָדָם",
"Huawei": "huawei", "Huawei": "huawei",
"High": "High", "High": "High",
"Retry": "נסה שוב", "Retry": "נסה שוב",

@ -420,8 +420,6 @@
"For safety, must use secret key": "Korištenje tajnog ključa je obavezno", "For safety, must use secret key": "Korištenje tajnog ključa je obavezno",
"Device Token": "Token uređaja", "Device Token": "Token uređaja",
"Platform": "Platforma", "Platform": "Platforma",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Visoko", "High": "Visoko",
"Retry": "Ponovnih pokušaja", "Retry": "Ponovnih pokušaja",

@ -418,8 +418,6 @@
"For safety, must use secret key": "Untuk keamaan Anda harus menggunakan kunci rahasia", "For safety, must use secret key": "Untuk keamaan Anda harus menggunakan kunci rahasia",
"Device Token": "Token Perangkat", "Device Token": "Token Perangkat",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Tinggi", "High": "Tinggi",
"Retry": "Ulang", "Retry": "Ulang",

@ -507,7 +507,6 @@
"lineDevConsoleTo": "Line Developers Console - {0}", "lineDevConsoleTo": "Line Developers Console - {0}",
"Basic Settings": "基本設定", "Basic Settings": "基本設定",
"User ID": "User ID", "User ID": "User ID",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"Device Token": "デバイストークン", "Device Token": "デバイストークン",
"recurringIntervalMessage": "毎日1回実行する{0} 日に1回実行する", "recurringIntervalMessage": "毎日1回実行する{0} 日に1回実行する",

@ -413,8 +413,6 @@
"For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.", "For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.",
"Device Token": "기기 Token", "Device Token": "기기 Token",
"Platform": "플랫폼", "Platform": "플랫폼",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "재시도", "Retry": "재시도",

@ -404,8 +404,6 @@
"For safety, must use secret key": "Voor de veiligheid moet je de secret key gebruiken", "For safety, must use secret key": "Voor de veiligheid moet je de secret key gebruiken",
"Device Token": "Apparaat Token", "Device Token": "Apparaat Token",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Hoog", "High": "Hoog",
"Retry": "Opnieuw", "Retry": "Opnieuw",

@ -414,8 +414,6 @@
"For safety, must use secret key": "Ze względów bezpieczeństwa musisz użyć tajnego klucza", "For safety, must use secret key": "Ze względów bezpieczeństwa musisz użyć tajnego klucza",
"Device Token": "Token urządzenia", "Device Token": "Token urządzenia",
"Platform": "Platforma", "Platform": "Platforma",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Wysoki", "High": "Wysoki",
"Retry": "Ponów", "Retry": "Ponów",

@ -523,7 +523,6 @@
"Example:": "Exemplo: {0}", "Example:": "Exemplo: {0}",
"Read more:": "Leia mais em: {0}", "Read more:": "Leia mais em: {0}",
"promosmsAllowLongSMS": "Permitir SMS grandes", "promosmsAllowLongSMS": "Permitir SMS grandes",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"smseagleTo": "Números Dos Telefones", "smseagleTo": "Números Dos Telefones",
"smseaglePriority": "Prioridade da mensagem (0-9, padrão=0)", "smseaglePriority": "Prioridade da mensagem (0-9, padrão=0)",

@ -421,8 +421,6 @@
"For safety, must use secret key": "В целях безопасности необходимо использовать секретный ключ", "For safety, must use secret key": "В целях безопасности необходимо использовать секретный ключ",
"Device Token": "Токен устройства", "Device Token": "Токен устройства",
"Platform": "Платформа", "Platform": "Платформа",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "Повторить", "Retry": "Повторить",

@ -404,8 +404,6 @@
"For safety, must use secret key": "เพื่อความปลอดภัย จำเป็นต้องตั้งค่ากุญแจการเข้าถึง", "For safety, must use secret key": "เพื่อความปลอดภัย จำเป็นต้องตั้งค่ากุญแจการเข้าถึง",
"Device Token": "Device Token", "Device Token": "Device Token",
"Platform": "แพลตฟอร์ม", "Platform": "แพลตฟอร์ม",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "สูง", "High": "สูง",
"Retry": "ลองใหม่", "Retry": "ลองใหม่",

@ -408,8 +408,6 @@
"For safety, must use secret key": "Güvenlik için gizli anahtar kullanılmalıdır", "For safety, must use secret key": "Güvenlik için gizli anahtar kullanılmalıdır",
"Device Token": "Cihaz Tokeni", "Device Token": "Cihaz Tokeni",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "Tekrar", "Retry": "Tekrar",

@ -413,8 +413,6 @@
"For safety, must use secret key": "Для безпеки необхідно використовувати секретний ключ", "For safety, must use secret key": "Для безпеки необхідно використовувати секретний ключ",
"Device Token": "Токен пристрою", "Device Token": "Токен пристрою",
"Platform": "Платформа", "Platform": "Платформа",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "Високий", "High": "Високий",
"Retry": "Повтор", "Retry": "Повтор",

@ -403,8 +403,6 @@
"For safety, must use secret key": "Để an toàn, hãy dùng secret key", "For safety, must use secret key": "Để an toàn, hãy dùng secret key",
"Device Token": "Device Token", "Device Token": "Device Token",
"Platform": "Platform", "Platform": "Platform",
"iOS": "iOS",
"Android": "Android",
"Huawei": "Huawei", "Huawei": "Huawei",
"High": "High", "High": "High",
"Retry": "Retry", "Retry": "Retry",

@ -452,8 +452,6 @@
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥", "For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
"Device Token": "Apple Device Token", "Device Token": "Apple Device Token",
"Platform": "平台", "Platform": "平台",
"iOS": "iOS",
"Android": "Android",
"Huawei": "华为", "Huawei": "华为",
"High": "高", "High": "高",
"Retry": "重试次数", "Retry": "重试次数",

@ -139,6 +139,8 @@
"Disable 2FA": "關閉 2FA", "Disable 2FA": "關閉 2FA",
"2FA Settings": "2FA 設定", "2FA Settings": "2FA 設定",
"Two Factor Authentication": "雙重認證", "Two Factor Authentication": "雙重認證",
"filterActive": "執行狀態",
"filterActivePaused": "已暫停",
"Active": "生效", "Active": "生效",
"Inactive": "未生效", "Inactive": "未生效",
"Token": "Token", "Token": "Token",
@ -692,7 +694,6 @@
"Retry": "重試", "Retry": "重試",
"High": "高", "High": "高",
"Huawei": "華為", "Huawei": "華為",
"Android": "Android",
"For safety, must use secret key": "為安全起見,必須使用 Secret Key", "For safety, must use secret key": "為安全起見,必須使用 Secret Key",
"SecretKey": "SecretKey", "SecretKey": "SecretKey",
"WebHookUrl": "WebHookUrl", "WebHookUrl": "WebHookUrl",

@ -445,8 +445,6 @@
"For safety, must use secret key": "為了安全起見,必須使用秘密金鑰", "For safety, must use secret key": "為了安全起見,必須使用秘密金鑰",
"Device Token": "裝置權杖", "Device Token": "裝置權杖",
"Platform": "平台", "Platform": "平台",
"iOS": "iOS",
"Android": "Android",
"Huawei": "華為", "Huawei": "華為",
"High": "高", "High": "高",
"Retry": "重試", "Retry": "重試",

@ -1,9 +1,12 @@
import axios from "axios"; import axios from "axios";
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend";
const env = process.env.NODE_ENV || "production"; const env = process.env.NODE_ENV || "production";
// change the axios base url for development // 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"; axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
} }

@ -4,6 +4,7 @@ import jwtDecode from "jwt-decode";
import Favico from "favico.js"; import Favico from "favico.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js";
const toast = useToast(); const toast = useToast();
let socket; let socket;
@ -98,7 +99,9 @@ export default {
let wsHost; let wsHost;
const env = process.env.NODE_ENV || "production"; 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"; wsHost = protocol + location.hostname + ":3001";
} else { } else {
wsHost = protocol + location.host; wsHost = protocol + location.host;
@ -698,9 +701,11 @@ export default {
stats() { stats() {
let result = { let result = {
active: 0,
up: 0, up: 0,
down: 0, down: 0,
maintenance: 0, maintenance: 0,
pending: 0,
unknown: 0, unknown: 0,
pause: 0, pause: 0,
}; };
@ -712,12 +717,13 @@ export default {
if (monitor && ! monitor.active) { if (monitor && ! monitor.active) {
result.pause++; result.pause++;
} else if (beat) { } else if (beat) {
result.active++;
if (beat.status === UP) { if (beat.status === UP) {
result.up++; result.up++;
} else if (beat.status === DOWN) { } else if (beat.status === DOWN) {
result.down++; result.down++;
} else if (beat.status === PENDING) { } else if (beat.status === PENDING) {
result.up++; result.pending++;
} else if (beat.status === MAINTENANCE) { } else if (beat.status === MAINTENANCE) {
result.maintenance++; result.maintenance++;
} else { } else {

@ -30,6 +30,9 @@ export default {
theme() { theme() {
// As entry can be status page now, set forceStatusPageTheme to true to use status page theme // As entry can be status page now, set forceStatusPageTheme to true to use status page theme
if (this.forceStatusPageTheme) { if (this.forceStatusPageTheme) {
if (this.statusPageTheme === "auto") {
return this.system;
}
return this.statusPageTheme; return this.statusPageTheme;
} }

@ -8,12 +8,20 @@
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
</div> </div>
<p class="url"> <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 === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
<br> <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>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }} <span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br> <br>
@ -432,7 +440,7 @@ export default {
translationPrefix = "Avg. "; 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"); return this.$t(translationPrefix + "Response");
} }
@ -582,6 +590,10 @@ table {
color: $dark-font-color; color: $dark-font-color;
} }
.keyword-inverted {
color: $dark-font-color;
}
.dropdown-clear-data { .dropdown-clear-data {
ul { ul {
background-color: $dark-bg; background-color: $dark-bg;

@ -27,6 +27,9 @@
<option value="keyword"> <option value="keyword">
HTTP(s) - {{ $t("Keyword") }} HTTP(s) - {{ $t("Keyword") }}
</option> </option>
<option value="json-query">
HTTP(s) - {{ $t("Json Query") }}
</option>
<option value="grpc-keyword"> <option value="grpc-keyword">
gRPC(s) - {{ $t("Keyword") }} gRPC(s) - {{ $t("Keyword") }}
</option> </option>
@ -58,6 +61,9 @@
<option value="mqtt"> <option value="mqtt">
MQTT MQTT
</option> </option>
<option value="kafka-producer">
Kafka Producer
</option>
<option value="sqlserver"> <option value="sqlserver">
Microsoft SQL Server Microsoft SQL Server
</option> </option>
@ -76,10 +82,17 @@
<option value="redis"> <option value="redis">
Redis Redis
</option> </option>
<option value="tailscale-ping">
Tailscale Ping
</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div v-if="monitor.type === 'tailscale-ping'" class="alert alert-warning" role="alert">
{{ $t("tailscalePingWarning") }}
</div>
<!-- Friendly Name --> <!-- Friendly Name -->
<div class="my-3"> <div class="my-3">
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label> <label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
@ -97,7 +110,7 @@
</div> </div>
<!-- URL --> <!-- 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> <label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
@ -127,6 +140,31 @@
</div> </div>
</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 --> <!-- Game -->
<!-- GameDig only --> <!-- GameDig only -->
<div v-if="monitor.type === 'gamedig'" class="my-3"> <div v-if="monitor.type === 'gamedig'" class="my-3">
@ -138,9 +176,60 @@
</select> </select>
</div> </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 --> <!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only --> <!-- 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'" class="my-3"> <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> <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> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div> </div>
@ -356,7 +445,7 @@
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <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"> <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification"> <label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }} {{ $t("Certificate Expiry Notification") }}
@ -365,7 +454,7 @@
</div> </div>
</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=""> <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls"> <label class="form-check-label" for="ignore-tls">
{{ $t("ignoreTLSError") }} {{ $t("ignoreTLSError") }}
@ -457,7 +546,7 @@
</button> </button>
<!-- Proxies --> <!-- 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> <h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
<p v-if="$root.proxyList.length === 0"> <p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }} {{ $t("Not available, please setup.") }}
@ -484,8 +573,58 @@
</button> </button>
</div> </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 --> <!-- 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> <h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
<!-- Method --> <!-- Method -->
@ -696,6 +835,7 @@ export default {
}, },
acceptedStatusCodeOptions: [], acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [], dnsresolvetypeOptions: [],
kafkaSaslMechanismOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(), ipOrHostnameRegexPattern: hostNameRegexPattern(),
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true), mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
gameList: null, gameList: null,
@ -959,12 +1099,21 @@ message HealthCheckResponse {
"TXT", "TXT",
]; ];
let kafkaSaslMechanismOptions = [
"None",
"plain",
"scram-sha-256",
"scram-sha-512",
"aws",
];
for (let i = 100; i <= 999; i++) { for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString()); acceptedStatusCodeOptions.push(i.toString());
} }
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions; this.dnsresolvetypeOptions = dnsresolvetypeOptions;
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
}, },
methods: { methods: {
/** Initialize the edit monitor form */ /** Initialize the edit monitor form */
@ -998,7 +1147,11 @@ message HealthCheckResponse {
mqttTopic: "", mqttTopic: "",
mqttSuccessMessage: "", mqttSuccessMessage: "",
authMethod: null, authMethod: null,
httpBodyEncoding: "json" httpBodyEncoding: "json",
kafkaProducerBrokers: [],
kafkaProducerSaslOptions: {
mechanism: "None",
},
}; };
if (this.$root.proxyList && !this.monitor.proxyId) { if (this.$root.proxyList && !this.monitor.proxyId) {
@ -1039,6 +1192,7 @@ message HealthCheckResponse {
this.monitor.childrenIDs = undefined; this.monitor.childrenIDs = undefined;
this.monitor.forceInactive = undefined; this.monitor.forceInactive = undefined;
this.monitor.pathName = undefined; this.monitor.pathName = undefined;
this.monitor.screenshot = undefined;
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]); this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => { 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 * Validate form input
* @returns {boolean} Is the form input valid? * @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); 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; this.monitor.httpBodyEncoding = null;
} }

@ -116,12 +116,6 @@ export default {
backup: { backup: {
title: this.$t("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: { about: {
title: this.$t("About"), title: this.$t("About"),
}, },

@ -325,7 +325,7 @@
</p> </p>
<div class="refresh-info mb-2"> <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>{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
</div> </div>
</footer> </footer>
@ -360,7 +360,6 @@ import DOMPurify from "dompurify";
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
import PublicGroupList from "../components/PublicGroupList.vue"; import PublicGroupList from "../components/PublicGroupList.vue";
import MaintenanceTime from "../components/MaintenanceTime.vue"; import MaintenanceTime from "../components/MaintenanceTime.vue";
import DateTime from "../components/Datetime.vue";
import { getResBaseURL } from "../util-frontend"; 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 { 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"; import Tag from "../components/Tag.vue";
@ -386,7 +385,6 @@ export default {
Confirm, Confirm,
PrismEditor, PrismEditor,
MaintenanceTime, MaintenanceTime,
DateTime,
Tag, Tag,
VueMultiselect VueMultiselect
}, },
@ -583,6 +581,10 @@ export default {
return ""; return "";
} }
}, },
lastUpdateTimeDisplay() {
return this.$root.datetime(this.lastUpdateTime);
}
}, },
watch: { watch: {

@ -19,7 +19,6 @@ import DockerHosts from "./components/settings/Docker.vue";
import MaintenanceDetails from "./pages/MaintenanceDetails.vue"; import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
import ManageMaintenance from "./pages/ManageMaintenance.vue"; import ManageMaintenance from "./pages/ManageMaintenance.vue";
import APIKeys from "./components/settings/APIKeys.vue"; import APIKeys from "./components/settings/APIKeys.vue";
import Plugins from "./components/settings/Plugins.vue";
import SetupDatabase from "./pages/SetupDatabase.vue"; import SetupDatabase from "./pages/SetupDatabase.vue";
// Settings - Sub Pages // Settings - Sub Pages
@ -131,10 +130,6 @@ const routes = [
path: "backup", path: "backup",
component: Backup, component: Backup,
}, },
{
path: "plugins",
component: Plugins,
},
{ {
path: "about", path: "about",
component: About, component: About,

@ -72,13 +72,32 @@ export function setPageLocale() {
*/ */
export function getResBaseURL() { export function getResBaseURL() {
const env = process.env.NODE_ENV; 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"; return location.protocol + "//" + location.hostname + ":3001";
} else { } else {
return ""; 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 * @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

Loading…
Cancel
Save