mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-15 16:07:03 +08:00
Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
11d01ebc78 | ||
|
66cfbd02c3 | ||
|
688f23035b | ||
|
7701e2ad36 | ||
|
8e72d6f534 | ||
|
278b88a9d9 | ||
|
084cf01fcd | ||
|
25c8196641 | ||
|
baf5613dfa | ||
|
695691468c | ||
|
4891ec4527 | ||
|
e2a87eb430 | ||
|
80927332cb | ||
|
a0eb733d54 | ||
|
21d556528f | ||
|
357466cc90 | ||
|
b038d09349 | ||
|
5dd4231e56 | ||
|
c6d0c431bd | ||
|
d1b7f4c834 | ||
|
5c4180fb45 | ||
|
345e61abca | ||
|
dd1526deff | ||
|
be26bb75d9 | ||
|
99fb5836e2 | ||
|
2f5a565ce4 | ||
|
973db9d4b2 | ||
|
6bece8796e | ||
|
e7d1b4e14a | ||
|
e5c6783781 | ||
|
ac68a35d3a | ||
|
d825dbf828 | ||
|
cfb4bbc6cb | ||
|
293015ff35 | ||
|
18d8b3a8e0 | ||
|
d55794e1a5 | ||
|
3d50572dd7 | ||
|
cdb38d49eb | ||
|
80b55786a4 | ||
|
fe40d819bd | ||
|
3dbd8277f0 | ||
|
771d21c4ad | ||
|
ed6b4e5ae5 | ||
|
3b9c95a8a8 | ||
|
cdf6922bdd | ||
|
9954ba82e7 | ||
|
19873e5b9e | ||
|
13ae878ee8 | ||
|
1774bb86dc | ||
|
c583037dff | ||
|
9da712054a | ||
|
af78da1dd9 | ||
|
9e041f219b | ||
|
8c60e902e1 | ||
|
de74efb2e6 | ||
|
9ee2780e9e | ||
|
a386f1fc9e | ||
|
35154ef9c5 | ||
|
1baa592824 | ||
|
9882fc65b1 | ||
|
3e5e7e6e32 | ||
|
0e725569e5 | ||
|
afcfb7e19c | ||
|
eaee55fc8f | ||
|
affac0a97b | ||
|
a12e7eba72 | ||
|
4f6035899d | ||
|
dd77baabe1 | ||
|
820f2eec9f | ||
|
f8c9a20afd | ||
|
cea894cc6d | ||
|
79b38e0e7b | ||
|
7cc9783436 | ||
|
8955c3816b | ||
|
7761e9a05e | ||
|
c9d6e576ab | ||
|
97d38ee1a8 | ||
|
bdcbd6389b | ||
|
a00561ff09 | ||
|
6708eed121 | ||
|
3c56a6f395 | ||
|
d37c33ad42 | ||
|
8897385690 | ||
|
6132a45c7c | ||
|
f68452c47a | ||
|
9d71e34a83 | ||
|
1fa8c0f9fe | ||
|
9a8bea5761 | ||
|
376d84c742 | ||
|
f3fe392ec4 | ||
|
9c3bb67b6b | ||
|
5200e10aab | ||
|
f1a396b0f7 | ||
|
9825b33ef3 | ||
|
00f733d352 | ||
|
fd10897988 | ||
|
317024ed72 | ||
|
f604d96c5b | ||
|
f30f00655f | ||
|
891f09def7 | ||
|
6b5e179bb0 | ||
|
f653aba735 | ||
|
682f8e52a8 | ||
|
171aff1226 | ||
|
bb7de6aa88 | ||
|
6de0c6a90c | ||
|
94b69935fe | ||
|
3f30feaefb |
28
.devcontainer/README.md
Normal file
28
.devcontainer/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Codespaces
|
||||
|
||||
You can modifiy Uptime Kuma in your browser without setting up a local development.
|
||||
|
||||

|
||||
|
||||
1. Click `Code` -> `Create codespace on master`
|
||||
2. Wait a few minutes until you see there are two exposed ports
|
||||
3. Go to the `3000` url, see if it is working
|
||||
|
||||

|
||||
|
||||
## Frontend
|
||||
|
||||
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
|
||||
You don't need to restart the frontend, unless you try to add a new frontend dependency.
|
||||
|
||||
## Backend
|
||||
|
||||
The backend does not automatically hot-reload.
|
||||
You will need to restart the backend after changing something using these steps:
|
||||
|
||||
1. Click `Terminal`
|
||||
2. Click `Codespaces: server-dev` in the right panel
|
||||
3. Press `Ctrl + C` to stop the server
|
||||
4. Press `Up` to run `npm run start-server-dev`
|
||||
|
||||

|
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"updateContentCommand": "npm ci",
|
||||
"postCreateCommand": "",
|
||||
"postAttachCommand": {
|
||||
"frontend-dev": "npm run start-frontend-devcontainer",
|
||||
"server-dev": "npm run start-server-dev",
|
||||
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000, 3001]
|
||||
}
|
4
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
4
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -44,7 +44,7 @@ body:
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: "💻 Operating System and Arch"
|
||||
description: "Which OS is your server/device running on?"
|
||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||
validations:
|
||||
required: true
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
id: browser-vendor
|
||||
attributes:
|
||||
label: "🌐 Browser"
|
||||
description: "Which browser are you running on?"
|
||||
description: "Which browser are you running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||
validations:
|
||||
required: true
|
||||
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Auto Test
|
||||
@@ -21,8 +21,8 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node: [ 14, 16, 18, 20 ]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 14, 18 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
@@ -41,6 +41,29 @@ jobs:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||
|
||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||
armv7-simple-test:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ARMv7 ]
|
||||
node: [ 14.21.3, 18.16.1 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm ci --production
|
||||
|
||||
check-linters:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -52,7 +75,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
|
||||
@@ -67,7 +89,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:test
|
||||
@@ -83,7 +104,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:run:unit
|
||||
|
14
README.md
14
README.md
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||
* Fancy, Reactive, Fast UI/UX
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
||||
* 20 second intervals
|
||||
@@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Requirements:
|
||||
Requirements:
|
||||
- Platform
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
||||
- ❌ Replit / Heroku
|
||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported)
|
||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
|
||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
|
||||
```bash
|
||||
@@ -71,7 +71,7 @@ npm run setup
|
||||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have it:
|
||||
# Install PM2 if you don't have it:
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
|
||||
# Start Server
|
||||
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
|
||||
|
||||
### Windows Portable (x64)
|
||||
|
||||
https://github.com/louislam/uptime-kuma/releases/download/1.21.0/uptime-kuma-win64-portable-1.0.0.zip
|
||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
import visualizer from "rollup-plugin-visualizer";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
@@ -16,8 +17,12 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||
},
|
||||
plugins: [
|
||||
commonjs(),
|
||||
vue(),
|
||||
legacy({
|
||||
targets: [ "since 2015" ],
|
||||
@@ -42,6 +47,9 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [ /.js$/ ],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||
|
7
db/patch-add-invert-keyword.sql
Normal file
7
db/patch-add-invert-keyword.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD invert_keyword BOOLEAN default 0 not null;
|
||||
|
||||
COMMIT;
|
10
db/patch-added-json-query.sql
Normal file
10
db/patch-added-json-query.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD json_path TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD expected_value VARCHAR(255);
|
||||
|
||||
COMMIT;
|
22
db/patch-added-kafka-producer.sql
Normal file
22
db/patch-added-kafka-producer.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_topic VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_brokers TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_ssl INTEGER;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_sasl_options TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_message TEXT;
|
||||
|
||||
COMMIT;
|
@@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
|
||||
FROM louislam/uptime-kuma:base-debian AS release
|
||||
WORKDIR /app
|
||||
|
||||
ENV UPTIME_KUMA_IS_CONTAINER=1
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
@@ -70,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 3000 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
||||
CMD ["npm", "run", "start-pr-test"]
|
||||
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura />
|
||||
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
||||
</Weavers>
|
@@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyCompany("Uptime Kuma")]
|
||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
@@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
|
||||
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.1.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.1.0")]
|
||||
|
4102
package-lock.json
generated
4102
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -7,7 +7,7 @@
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.* || 16.* || 18.*"
|
||||
"node": "14 || 16 || 18 || >= 20.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install",
|
||||
@@ -19,6 +19,7 @@
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
@@ -69,7 +70,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
"@louislam/ping": "~0.4.4-mod.0",
|
||||
"@louislam/ping": "~0.4.4-mod.1",
|
||||
"@louislam/sqlite3": "15.1.6",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.27.0",
|
||||
@@ -97,9 +98,12 @@
|
||||
"https-proxy-agent": "~5.0.1",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
"limiter": "~2.1.0",
|
||||
"liquidjs": "^10.7.0",
|
||||
"mongodb": "~4.14.0",
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
@@ -112,12 +116,15 @@
|
||||
"password-hash": "~1.2.2",
|
||||
"pg": "~8.8.0",
|
||||
"pg-connection-string": "~2.5.0",
|
||||
"playwright-core": "~1.35.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"protobufjs": "~7.1.1",
|
||||
"protobufjs": "~7.2.4",
|
||||
"qs": "~6.10.4",
|
||||
"queue": "~7.0.0",
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
@@ -127,7 +134,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
"@babel/eslint-parser": "~7.17.0",
|
||||
"@babel/eslint-parser": "^7.22.7",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
@@ -135,9 +142,9 @@
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~2.1.0",
|
||||
"@vitejs/plugin-vue": "~3.1.0",
|
||||
"@vue/compiler-sfc": "~3.2.36",
|
||||
"@vitejs/plugin-legacy": "~4.1.0",
|
||||
"@vitejs/plugin-vue": "~4.2.3",
|
||||
"@vue/compiler-sfc": "~3.3.4",
|
||||
"@vuepic/vue-datepicker": "~3.4.8",
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
@@ -148,16 +155,16 @@
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"cypress": "^10.1.0",
|
||||
"cypress": "^12.17.0",
|
||||
"delay": "^5.0.0",
|
||||
"dns2": "~2.0.1",
|
||||
"dompurify": "~2.4.3",
|
||||
"eslint": "~8.14.0",
|
||||
"eslint-plugin-vue": "~8.7.1",
|
||||
"favico.js": "~0.3.10",
|
||||
"jest": "~27.2.5",
|
||||
"jest": "~29.6.1",
|
||||
"marked": "~4.2.5",
|
||||
"node-ssh": "~13.0.1",
|
||||
"node-ssh": "~13.1.0",
|
||||
"postcss-html": "~1.5.0",
|
||||
"postcss-rtlcss": "~3.7.2",
|
||||
"postcss-scss": "~4.0.4",
|
||||
@@ -165,15 +172,16 @@
|
||||
"qrcode": "~1.5.0",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~15.9.0",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-standard": "~25.0.0",
|
||||
"terser": "~5.15.0",
|
||||
"timezones-list": "~3.0.1",
|
||||
"typescript": "~4.4.4",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~3.2.7",
|
||||
"vite": "~4.4.1",
|
||||
"vite-plugin-commonjs": "^0.8.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "~3.2.47",
|
||||
"vue": "~3.3.4",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
|
@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @param {boolean} hideVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendInfo(socket) {
|
||||
async function sendInfo(socket, hideVersion = false) {
|
||||
let version;
|
||||
let latestVersion;
|
||||
|
||||
if (!hideVersion) {
|
||||
version = checkVersion.version;
|
||||
latestVersion = checkVersion.latestVersion;
|
||||
}
|
||||
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
version,
|
||||
latestVersion,
|
||||
primaryBaseURL: await setting("primaryBaseURL"),
|
||||
serverTimezone: await server.getTimezone(),
|
||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const args = require("args-parser")(process.argv);
|
||||
// Interop with browser
|
||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
|
@@ -4,7 +4,6 @@ const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const knex = require("knex");
|
||||
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
*/
|
||||
@@ -22,6 +21,8 @@ class Database {
|
||||
*/
|
||||
static uploadDir;
|
||||
|
||||
static screenshotDir;
|
||||
|
||||
static path;
|
||||
|
||||
/**
|
||||
@@ -70,6 +71,9 @@ class Database {
|
||||
"patch-monitor-tls.sql": true,
|
||||
"patch-maintenance-cron.sql": true,
|
||||
"patch-add-parent-monitor.sql": true,
|
||||
"patch-add-invert-keyword.sql": true,
|
||||
"patch-added-json-query.sql": true,
|
||||
"patch-added-kafka-producer.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,6 +103,12 @@ class Database {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create screenshot dir
|
||||
Database.screenshotDir = Database.dataDir + "screenshots/";
|
||||
if (! fs.existsSync(Database.screenshotDir)) {
|
||||
fs.mkdirSync(Database.screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
@@ -155,12 +165,12 @@ class Database {
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
||||
|
||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||
// FULL synchronous is very safe, but it is also slower.
|
||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||
await R.exec("PRAGMA synchronous = FULL");
|
||||
await R.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
if (!noLog) {
|
||||
log.info("db", "SQLite config:");
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const { clearOldData } = require("./jobs/clear-old-data");
|
||||
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
|
||||
const Cron = require("croner");
|
||||
|
||||
const jobs = [
|
||||
@@ -9,6 +10,12 @@ const jobs = [
|
||||
jobFunc: clearOldData,
|
||||
croner: null,
|
||||
},
|
||||
{
|
||||
name: "incremental-vacuum",
|
||||
interval: "*/5 * * * *",
|
||||
jobFunc: incrementalVacuum,
|
||||
croner: null,
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -39,6 +39,8 @@ const clearOldData = async () => {
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[ parsedPeriod ]
|
||||
);
|
||||
|
||||
await R.exec("PRAGMA optimize;");
|
||||
} catch (e) {
|
||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
|
21
server/jobs/incremental-vacuum.js
Normal file
21
server/jobs/incremental-vacuum.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
/**
|
||||
* Run incremental_vacuum and checkpoint the WAL.
|
||||
* @return {Promise<void>} A promise that resolves when the process is finished.
|
||||
*/
|
||||
|
||||
const incrementalVacuum = async () => {
|
||||
try {
|
||||
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
|
||||
await R.exec("PRAGMA incremental_vacuum(200)");
|
||||
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
||||
} catch (e) {
|
||||
log.error("incrementalVacuum", `Failed: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
incrementalVacuum,
|
||||
};
|
@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@@ -20,6 +20,8 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
const jsonata = require("jsonata");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -70,6 +72,12 @@ class Monitor extends BeanModel {
|
||||
|
||||
const tags = await this.getTags();
|
||||
|
||||
let screenshot = null;
|
||||
|
||||
if (this.type === "real-browser") {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
}
|
||||
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
@@ -90,6 +98,7 @@ class Monitor extends BeanModel {
|
||||
retryInterval: this.retryInterval,
|
||||
resendInterval: this.resendInterval,
|
||||
keyword: this.keyword,
|
||||
invertKeyword: this.isInvertKeyword(),
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
@@ -117,7 +126,15 @@ class Monitor extends BeanModel {
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
httpBodyEncoding: this.httpBodyEncoding,
|
||||
jsonPath: this.jsonPath,
|
||||
expectedValue: this.expectedValue,
|
||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||
screenshot,
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -141,6 +158,7 @@ class Monitor extends BeanModel {
|
||||
tlsCa: this.tlsCa,
|
||||
tlsCert: this.tlsCert,
|
||||
tlsKey: this.tlsKey,
|
||||
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,7 +173,7 @@ class Monitor extends BeanModel {
|
||||
async isActive() {
|
||||
const parentActive = await Monitor.isParentActive(this.id);
|
||||
|
||||
return this.active && parentActive;
|
||||
return (this.active === 1) && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +217,14 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInvertKeyword() {
|
||||
return Boolean(this.invertKeyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
@@ -303,7 +329,7 @@ class Monitor extends BeanModel {
|
||||
bean.msg = "Group empty";
|
||||
}
|
||||
|
||||
} else if (this.type === "http" || this.type === "keyword") {
|
||||
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@@ -431,7 +457,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
} else {
|
||||
} else if (this.type === "keyword") {
|
||||
|
||||
let data = res.data;
|
||||
|
||||
@@ -440,17 +466,37 @@ class Monitor extends BeanModel {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found";
|
||||
let keywordFound = data.includes(this.keyword);
|
||||
if (keywordFound === !this.isInvertKeyword()) {
|
||||
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||
if (data.length > 50) {
|
||||
data = data.substring(0, 47) + "...";
|
||||
}
|
||||
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||
throw new Error(bean.msg + ", but keyword is " +
|
||||
(keywordFound ? "present" : "not") + " in [" + data + "]");
|
||||
}
|
||||
|
||||
} else if (this.type === "json-query") {
|
||||
let data = res.data;
|
||||
|
||||
// convert data to object
|
||||
if (typeof data === "string") {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
|
||||
let expression = jsonata(this.jsonPath);
|
||||
|
||||
let result = await expression.evaluate(data);
|
||||
|
||||
if (result.toString() === this.expectedValue) {
|
||||
bean.msg += ", expected value is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
@@ -525,7 +571,7 @@ class Monitor extends BeanModel {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
retries = 0;
|
||||
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||
this.heartbeatInterval = setTimeout(safeBeat, timeout);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -618,9 +664,15 @@ class Monitor extends BeanModel {
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Status;
|
||||
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
|
||||
bean.status = PENDING;
|
||||
bean.msg = res.data.State.Health.Status;
|
||||
} else {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
|
||||
}
|
||||
} else {
|
||||
throw Error("Container State is " + res.data.State.Status);
|
||||
}
|
||||
@@ -649,7 +701,6 @@ class Monitor extends BeanModel {
|
||||
grpcEnableTls: this.grpcEnableTls,
|
||||
grpcMethod: this.grpcMethod,
|
||||
grpcBody: this.grpcBody,
|
||||
keyword: this.keyword
|
||||
};
|
||||
const response = await grpcQuery(options);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
@@ -662,13 +713,14 @@ class Monitor extends BeanModel {
|
||||
bean.status = DOWN;
|
||||
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||
} else {
|
||||
if (response.data.toString().includes(this.keyword)) {
|
||||
let keywordFound = response.data.toString().includes(this.keyword);
|
||||
if (keywordFound === !this.isInvertKeyword()) {
|
||||
bean.status = UP;
|
||||
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
||||
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||
} else {
|
||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
||||
bean.status = DOWN;
|
||||
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
||||
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
||||
}
|
||||
}
|
||||
} else if (this.type === "postgres") {
|
||||
@@ -715,7 +767,8 @@ class Monitor extends BeanModel {
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret,
|
||||
port
|
||||
port,
|
||||
this.interval * 1000 * 0.8,
|
||||
);
|
||||
if (resp.code) {
|
||||
bean.msg = resp.code;
|
||||
@@ -740,11 +793,29 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
await monitorType.check(this, bean);
|
||||
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
|
||||
if (!bean.ping) {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
}
|
||||
|
||||
} else if (this.type === "kafka-producer") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await kafkaProducerAsync(
|
||||
JSON.parse(this.kafkaProducerBrokers),
|
||||
this.kafkaProducerTopic,
|
||||
this.kafkaProducerMessage,
|
||||
{
|
||||
allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
|
||||
ssl: this.kafkaProducerSsl,
|
||||
clientId: `Uptime-Kuma/${version}`,
|
||||
interval: this.interval,
|
||||
},
|
||||
JSON.parse(this.kafkaProducerSaslOptions),
|
||||
);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else {
|
||||
throw new Error("Unknown Monitor Type");
|
||||
}
|
||||
|
@@ -6,9 +6,10 @@ class MonitorType {
|
||||
*
|
||||
* @param {Monitor} monitor
|
||||
* @param {Heartbeat} heartbeat
|
||||
* @param {UptimeKumaServer} server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
async check(monitor, heartbeat, server) {
|
||||
throw new Error("You need to override check()");
|
||||
}
|
||||
|
||||
|
212
server/monitor-types/real-browser-monitor-type.js
Normal file
212
server/monitor-types/real-browser-monitor-type.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { chromium } = require("playwright-core");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const { Settings } = require("../settings");
|
||||
const commandExistsSync = require("command-exists").sync;
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
|
||||
let browser = null;
|
||||
|
||||
let allowedList = [];
|
||||
let lastAutoDetectChromeExecutable = null;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
|
||||
// Allow Chromium too
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||
|
||||
// For Loop A to Z
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
let drive = String.fromCharCode(i);
|
||||
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
||||
}
|
||||
|
||||
} else if (process.platform === "linux") {
|
||||
allowedList = [
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/google-chrome",
|
||||
];
|
||||
} else if (process.platform === "darwin") {
|
||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||
allowedList = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
];
|
||||
}
|
||||
|
||||
log.debug("chrome", allowedList);
|
||||
|
||||
async function isAllowedChromeExecutable(executablePath) {
|
||||
console.log(config.args);
|
||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the executablePath is in the list of allowed executables
|
||||
return allowedList.includes(executablePath);
|
||||
}
|
||||
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
browser = await chromium.launch({
|
||||
//headless: false,
|
||||
executablePath,
|
||||
});
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
async function prepareChromeExecutable(executablePath) {
|
||||
// Special code for using the playwright_chromium
|
||||
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
||||
// Set to undefined = use playwright_chromium
|
||||
executablePath = undefined;
|
||||
} else if (!executablePath) {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
|
||||
// Install chromium in container via apt install
|
||||
if ( !commandExistsSync(executablePath)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
log.info("Chromium", "Installing Chromium...");
|
||||
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
||||
|
||||
// On exit
|
||||
child.on("exit", (code) => {
|
||||
log.info("Chromium", "apt install chromium exited with code " + code);
|
||||
|
||||
if (code === 0) {
|
||||
log.info("Chromium", "Installed Chromium");
|
||||
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
|
||||
log.info("Chromium", "Chromium version: " + version);
|
||||
resolve();
|
||||
} else if (code === 100) {
|
||||
reject(new Error("Installing Chromium, please wait..."));
|
||||
} else {
|
||||
reject(new Error("apt install chromium failed with code " + code));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
executablePath = findChrome(allowedList);
|
||||
}
|
||||
} else {
|
||||
// User specified a path
|
||||
// Check if the executablePath is in the list of allowed
|
||||
if (!await isAllowedChromeExecutable(executablePath)) {
|
||||
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
|
||||
}
|
||||
}
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
function findChrome(executables) {
|
||||
// Use the last working executable, so we don't have to search for it again
|
||||
if (lastAutoDetectChromeExecutable) {
|
||||
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
||||
return lastAutoDetectChromeExecutable;
|
||||
}
|
||||
}
|
||||
|
||||
for (let executable of executables) {
|
||||
if (commandExistsSync(executable)) {
|
||||
lastAutoDetectChromeExecutable = executable;
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
||||
}
|
||||
|
||||
async function resetChrome() {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the chrome executable is valid and return the version
|
||||
* @param executablePath
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function testChrome(executablePath) {
|
||||
try {
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
log.info("Chromium", "Testing Chromium executable: " + executablePath);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath,
|
||||
});
|
||||
const version = browser.version();
|
||||
await browser.close();
|
||||
return version;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
||||
*
|
||||
*/
|
||||
class RealBrowserMonitorType extends MonitorType {
|
||||
|
||||
name = "real-browser";
|
||||
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
const res = await page.goto(monitor.url, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: monitor.interval * 1000 * 0.8,
|
||||
});
|
||||
|
||||
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(Database.screenshotDir, filename),
|
||||
});
|
||||
|
||||
await context.close();
|
||||
|
||||
if (res.status() >= 200 && res.status() < 400) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = res.status();
|
||||
|
||||
const timing = res.request().timing();
|
||||
heartbeat.ping = timing.responseEnd;
|
||||
} else {
|
||||
throw new Error(res.status() + "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RealBrowserMonitorType,
|
||||
testChrome,
|
||||
resetChrome,
|
||||
};
|
@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
|
@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
if (notification.slackchannelnotify) {
|
||||
msg += " <!channel>";
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": textMsg,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
42
server/notification-providers/smsc.js
Normal file
42
server/notification-providers/smsc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSC extends NotificationProvider {
|
||||
name = "smsc";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
|
||||
let getArray = [
|
||||
"fmt=3",
|
||||
"translit=" + notification.smscTranslit,
|
||||
"login=" + notification.smscLogin,
|
||||
"psw=" + notification.smscPassword,
|
||||
"phones=" + notification.smscToNumber,
|
||||
"mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
|
||||
];
|
||||
if (notification.smscSenderName !== "") {
|
||||
getArray.push("sender=" + notification.smscSenderName);
|
||||
}
|
||||
|
||||
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
|
||||
if (resp.data.id === undefined) {
|
||||
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMSC;
|
@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
|
@@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
let accountSID = notification.twilioAccountSID;
|
||||
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
|
||||
let authToken = notification.twilioAuthToken;
|
||||
|
||||
try {
|
||||
@@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
|
||||
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const FormData = require("form-data");
|
||||
const { Liquid } = require("liquidjs");
|
||||
|
||||
class Webhook extends NotificationProvider {
|
||||
|
||||
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let finalData;
|
||||
let config = {
|
||||
headers: {}
|
||||
};
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
config.headers = finalData.getHeaders();
|
||||
} else {
|
||||
finalData = data;
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
config.headers = formData.getHeaders();
|
||||
data = formData;
|
||||
} else if (notification.webhookContentType === "custom") {
|
||||
// Initialize LiquidJS and parse the custom Body Template
|
||||
const engine = new Liquid();
|
||||
const tpl = engine.parse(notification.webhookCustomBody);
|
||||
|
||||
// Insert templated values into Body
|
||||
data = await engine.render(tpl,
|
||||
{
|
||||
msg,
|
||||
heartbeatJSON,
|
||||
monitorJSON
|
||||
});
|
||||
}
|
||||
|
||||
if (notification.webhookAdditionalHeaders) {
|
||||
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config);
|
||||
await axios.post(notification.webhookURL, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
|
@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const Apprise = require("./notification-providers/apprise");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const SMSC = require("./notification-providers/smsc");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
@@ -68,6 +69,7 @@ class Notification {
|
||||
new Apprise(),
|
||||
new Bark(),
|
||||
new ClickSendSMS(),
|
||||
new SMSC(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Feishu(),
|
||||
|
@@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
|
||||
if (!tlsInfo.valid) {
|
||||
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
|
||||
badgeValues.message = "Bad Cert";
|
||||
badgeValues.color = badgeConstants.downColor;
|
||||
badgeValues.color = downColor;
|
||||
} else {
|
||||
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
|
||||
|
||||
|
@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const Monitor = require("../model/monitor");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
||||
}
|
||||
});
|
||||
|
||||
// overall status-page status badge
|
||||
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
const slug = request.params.slug;
|
||||
const statusPageID = await StatusPage.slugToID(slug);
|
||||
const {
|
||||
label,
|
||||
upColor = badgeConstants.defaultUpColor,
|
||||
downColor = badgeConstants.defaultDownColor,
|
||||
partialColor = "#F6BE00",
|
||||
maintenanceColor = "#808080",
|
||||
style = badgeConstants.defaultStyle
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
AND \`group\`.status_page_id = ?
|
||||
`, [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
let hasUp = false;
|
||||
let hasDown = false;
|
||||
let hasMaintenance = false;
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
// retrieve the latest heartbeat
|
||||
let beat = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
// to be sure, when corresponding monitor not found
|
||||
if (beat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
// handle status of beat
|
||||
if (beat[0].status === 3) {
|
||||
hasMaintenance = true;
|
||||
} else if (beat[0].status === 2) {
|
||||
// ignored
|
||||
} else if (beat[0].status === 1) {
|
||||
hasUp = true;
|
||||
} else {
|
||||
hasDown = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!hasUp && !hasDown && !hasMaintenance) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
|
||||
} else {
|
||||
if (hasMaintenance) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = maintenanceColor;
|
||||
badgeValues.message = "Maintenance";
|
||||
} else if (hasUp && !hasDown) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = upColor;
|
||||
badgeValues.message = "Up";
|
||||
} else if (hasUp && hasDown) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = partialColor;
|
||||
badgeValues.message = "Degraded";
|
||||
} else {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = downColor;
|
||||
badgeValues.message = "Down";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// build the svg based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
|
||||
} catch (error) {
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
@@ -15,18 +15,25 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
||||
require("dotenv").config();
|
||||
|
||||
// Check Node.js Version
|
||||
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||
const requiredVersion = 14;
|
||||
const nodeVersion = process.versions.node;
|
||||
|
||||
// Get the required Node.js version from package.json
|
||||
const requiredNodeVersions = require("../package.json").engines.node;
|
||||
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
||||
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||
|
||||
// See more: https://github.com/louislam/uptime-kuma/issues/3138
|
||||
if (nodeVersion >= 20) {
|
||||
console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
|
||||
const semver = require("semver");
|
||||
const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
|
||||
|
||||
// Exit Uptime Kuma immediately if the Node.js version is banned
|
||||
if (semver.satisfies(nodeVersion, bannedNodeVersions)) {
|
||||
console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (nodeVersion < requiredVersion) {
|
||||
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
|
||||
process.exit(-1);
|
||||
// Warning if the Node.js version is not in the support list, but it maybe still works
|
||||
if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
|
||||
console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||
}
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
@@ -148,6 +155,7 @@ const { generalSocketHandler } = require("./socket-handlers/general-socket-handl
|
||||
const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const apicache = require("./modules/apicache");
|
||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -160,12 +168,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
@@ -213,6 +215,7 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.post("/test-webhook", async (request, response) => {
|
||||
log.debug("test", request.headers);
|
||||
log.debug("test", request.body);
|
||||
@@ -267,7 +270,7 @@ let needSetup = false;
|
||||
log.info("server", "Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
sendInfo(socket);
|
||||
sendInfo(socket, true);
|
||||
|
||||
if (needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
@@ -284,7 +287,7 @@ let needSetup = false;
|
||||
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
let decoded = jwt.verify(token, server.jwtSecret);
|
||||
|
||||
log.info("auth", "Username from JWT: " + decoded.username);
|
||||
|
||||
@@ -355,7 +358,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,7 +388,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -640,6 +643,9 @@ let needSetup = false;
|
||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
delete monitor.accepted_statuscodes;
|
||||
|
||||
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||
|
||||
bean.import(monitor);
|
||||
bean.user_id = socket.userID;
|
||||
|
||||
@@ -717,6 +723,7 @@ let needSetup = false;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = parseInt(monitor.port);
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.invertKeyword = monitor.invertKeyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.expiryNotification = monitor.expiryNotification;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
@@ -751,6 +758,13 @@ let needSetup = false;
|
||||
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||
bean.radiusSecret = monitor.radiusSecret;
|
||||
bean.httpBodyEncoding = monitor.httpBodyEncoding;
|
||||
bean.expectedValue = monitor.expectedValue;
|
||||
bean.jsonPath = monitor.jsonPath;
|
||||
bean.kafkaProducerTopic = monitor.kafkaProducerTopic;
|
||||
bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||
|
||||
bean.validate();
|
||||
|
||||
@@ -905,6 +919,8 @@ let needSetup = false;
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
@@ -913,6 +929,10 @@ let needSetup = false;
|
||||
// Fix #2880
|
||||
apicache.clear();
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
@@ -1156,6 +1176,8 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
|
||||
await setSettings("general", data);
|
||||
server.entryPage = data.entryPage;
|
||||
|
||||
@@ -1166,6 +1188,12 @@ let needSetup = false;
|
||||
await server.setTimezone(data.serverTimezone);
|
||||
}
|
||||
|
||||
// If Chrome Executable is changed, need to reset the browser
|
||||
if (previousChromeExecutable !== data.chromeExecutable) {
|
||||
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
|
||||
await resetChrome();
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
@@ -1367,13 +1395,14 @@ let needSetup = false;
|
||||
maxretries: monitorListData[i].maxretries,
|
||||
port: monitorListData[i].port,
|
||||
keyword: monitorListData[i].keyword,
|
||||
invertKeyword: monitorListData[i].invertKeyword,
|
||||
ignoreTls: monitorListData[i].ignoreTls,
|
||||
upsideDown: monitorListData[i].upsideDown,
|
||||
maxredirects: monitorListData[i].maxredirects,
|
||||
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||
notificationIDList: {},
|
||||
notificationIDList: monitorListData[i].notificationIDList,
|
||||
proxy_id: monitorListData[i].proxy_id || null,
|
||||
};
|
||||
|
||||
@@ -1637,6 +1666,7 @@ async function afterLogin(socket, user) {
|
||||
socket.join(user.id);
|
||||
|
||||
let monitorList = await server.sendMonitorList(socket);
|
||||
sendInfo(socket);
|
||||
server.sendMaintenanceList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
@@ -1704,7 +1734,7 @@ async function initDatabase(testMode = false) {
|
||||
needSetup = true;
|
||||
}
|
||||
|
||||
jwtSecret = jwtSecretBean.value;
|
||||
server.jwtSecret = jwtSecretBean.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
|
||||
const { sendInfo } = require("../client");
|
||||
const { checkLogin } = require("../util-server");
|
||||
const GameResolver = require("gamedig/lib/GameResolver");
|
||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||
|
||||
let gameResolver = new GameResolver();
|
||||
let gameList = null;
|
||||
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("testChrome", (executable, callback) => {
|
||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||
testChrome(executable).then((version) => {
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Found Chromium/Chrome. Version: " + version,
|
||||
});
|
||||
}).catch((e) => {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -10,7 +10,7 @@ const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
// 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.
|
||||
@@ -54,6 +54,12 @@ class UptimeKumaServer {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
jwtSecret = null;
|
||||
|
||||
static getInstance(args) {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
@@ -91,11 +97,17 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Set Monitor Types
|
||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
/** Initialise app after the database has been set up */
|
||||
async initAfterDatabaseReady() {
|
||||
// Static
|
||||
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
process.env.TZ = await this.getTimezone();
|
||||
@@ -237,9 +249,9 @@ class UptimeKumaServer {
|
||||
|
||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||
|| socket.client.conn.request.headers["x-real-ip"]
|
||||
|| clientIP.replace(/^.*:/, "");
|
||||
|| clientIP.replace(/^::ffff:/, "");
|
||||
} else {
|
||||
return clientIP.replace(/^.*:/, "");
|
||||
return clientIP.replace(/^::ffff:/, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,13 +262,43 @@ class UptimeKumaServer {
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getTimezone() {
|
||||
// From process.env.TZ
|
||||
try {
|
||||
if (process.env.TZ) {
|
||||
this.checkTimezone(process.env.TZ);
|
||||
return process.env.TZ;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("timezone", e.message + " in process.env.TZ");
|
||||
}
|
||||
|
||||
let timezone = await Settings.get("serverTimezone");
|
||||
if (timezone) {
|
||||
return timezone;
|
||||
} else if (process.env.TZ) {
|
||||
return process.env.TZ;
|
||||
} else {
|
||||
return dayjs.tz.guess();
|
||||
|
||||
// From Settings
|
||||
try {
|
||||
log.debug("timezone", "Using timezone from settings: " + timezone);
|
||||
if (timezone) {
|
||||
this.checkTimezone(timezone);
|
||||
return timezone;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("timezone", e.message + " in settings");
|
||||
}
|
||||
|
||||
// Guess
|
||||
try {
|
||||
let guess = dayjs.tz.guess();
|
||||
log.debug("timezone", "Guessing timezone: " + guess);
|
||||
if (guess) {
|
||||
this.checkTimezone(guess);
|
||||
return guess;
|
||||
} else {
|
||||
return "UTC";
|
||||
}
|
||||
} catch (e) {
|
||||
// Guess failed, fall back to UTC
|
||||
log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
|
||||
return "UTC";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,11 +310,24 @@ class UptimeKumaServer {
|
||||
return dayjs().format("Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error if the timezone is invalid
|
||||
* @param timezone
|
||||
*/
|
||||
checkTimezone(timezone) {
|
||||
try {
|
||||
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
|
||||
} catch (e) {
|
||||
throw new Error("Invalid timezone:" + timezone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current server timezone and environment variables
|
||||
* @param {string} timezone
|
||||
*/
|
||||
async setTimezone(timezone) {
|
||||
this.checkTimezone(timezone);
|
||||
await Settings.set("serverTimezone", timezone, "general");
|
||||
process.env.TZ = timezone;
|
||||
dayjs.tz.setDefault(timezone);
|
||||
@@ -288,5 +343,5 @@ module.exports = {
|
||||
UptimeKumaServer
|
||||
};
|
||||
|
||||
// Must be at the end
|
||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
||||
// Must be at the end to avoid circular dependencies
|
||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||
|
@@ -28,8 +28,11 @@ const {
|
||||
} = require("node-radius-utils");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
// SASLOptions used in JSDoc
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { Kafka, SASLOptions } = require("kafkajs");
|
||||
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
* @returns {Promise<Bean>}
|
||||
@@ -196,6 +199,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Monitor Kafka using Producer
|
||||
* @param {string} topic Topic name to produce into
|
||||
* @param {string} message Message to produce
|
||||
* @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}]
|
||||
* Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and
|
||||
* interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma"
|
||||
* and ssl defaults to false)
|
||||
* @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':'
|
||||
* @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to
|
||||
* {})
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options;
|
||||
|
||||
let connectedToKafka = false;
|
||||
|
||||
const timeoutID = setTimeout(() => {
|
||||
log.debug("kafkaProducer", "KafkaProducer timeout triggered");
|
||||
connectedToKafka = true;
|
||||
reject(new Error("Timeout"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
if (saslOptions.mechanism === "None") {
|
||||
saslOptions = undefined;
|
||||
}
|
||||
|
||||
let client = new Kafka({
|
||||
brokers: brokers,
|
||||
clientId: clientId,
|
||||
sasl: saslOptions,
|
||||
retry: {
|
||||
retries: 0,
|
||||
},
|
||||
ssl: ssl,
|
||||
});
|
||||
|
||||
let producer = client.producer({
|
||||
allowAutoTopicCreation: allowAutoTopicCreation,
|
||||
retry: {
|
||||
retries: 0,
|
||||
}
|
||||
});
|
||||
|
||||
producer.connect().then(
|
||||
() => {
|
||||
try {
|
||||
producer.send({
|
||||
topic: topic,
|
||||
messages: [{
|
||||
value: message,
|
||||
}],
|
||||
});
|
||||
connectedToKafka = true;
|
||||
clearTimeout(timeoutID);
|
||||
resolve("Message sent successfully");
|
||||
} catch (e) {
|
||||
connectedToKafka = true;
|
||||
producer.disconnect();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Error sending message: " + e.message));
|
||||
}
|
||||
}
|
||||
).catch(
|
||||
(e) => {
|
||||
connectedToKafka = true;
|
||||
producer.disconnect();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Error in producer connection: " + e.message));
|
||||
}
|
||||
);
|
||||
|
||||
producer.on("producer.network.request_timeout", (_) => {
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.network.request_timeout"));
|
||||
});
|
||||
|
||||
producer.on("producer.disconnect", (_) => {
|
||||
if (!connectedToKafka) {
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.disconnect"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Use NTLM Auth for a http request.
|
||||
* @param {Object} options The http request options
|
||||
@@ -378,6 +469,7 @@ exports.mongodbPing = async function (connectionString) {
|
||||
* @param {string} callingStationId ID of calling station
|
||||
* @param {string} secret Secret to use
|
||||
* @param {number} [port=1812] Port to contact radius server on
|
||||
* @param {number} [timeout=2500] Timeout for connection to use
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
exports.radius = function (
|
||||
@@ -388,10 +480,12 @@ exports.radius = function (
|
||||
callingStationId,
|
||||
secret,
|
||||
port = 1812,
|
||||
timeout = 2500,
|
||||
) {
|
||||
const client = new radiusClient({
|
||||
host: hostname,
|
||||
hostPort: port,
|
||||
timeout: timeout,
|
||||
dictionaries: [ file ],
|
||||
});
|
||||
|
||||
@@ -413,12 +507,18 @@ exports.radius = function (
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn,
|
||||
url: dsn
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.connect().then(() => {
|
||||
if (!client.isOpen) {
|
||||
client.emit("error", new Error("connection isn't open"));
|
||||
}
|
||||
client.ping().then((res, err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
@@ -428,7 +528,7 @@ exports.redisPingAsync = function (dsn) {
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -436,12 +436,12 @@ optgroup {
|
||||
.monitor-list {
|
||||
&.scrollbar {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 65px);
|
||||
height: calc(100% - 107px);
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
&.scrollbar {
|
||||
height: calc(100% - 40px);
|
||||
height: calc(100% - 97px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -69,6 +69,7 @@
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
|
@@ -22,78 +22,78 @@
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
|
||||
<label for="duration" class="form-label">{{ $t("Badge Duration") }}</label>
|
||||
<input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required>
|
||||
<label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
|
||||
<input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
|
||||
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
|
||||
<input id="label" v-model="badge.label" type="text" class="form-control" required>
|
||||
<input id="label" v-model="badge.label" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
|
||||
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
|
||||
<input id="prefix" v-model="badge.prefix" type="text" class="form-control" required>
|
||||
<input id="prefix" v-model="badge.prefix" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
|
||||
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
|
||||
<input id="suffix" v-model="badge.suffix" type="text" class="form-control" required>
|
||||
<input id="suffix" v-model="badge.suffix" type="text" placeholder="%" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
|
||||
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
|
||||
<input id="labelColor" v-model="badge.labelColor" type="text" class="form-control" required>
|
||||
<input id="labelColor" v-model="badge.labelColor" type="text" placeholder="#555" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
|
||||
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
|
||||
<input id="color" v-model="badge.color" type="text" class="form-control" required>
|
||||
<input id="color" v-model="badge.color" type="text" :placeholder="badgeConstants.defaultUpColor" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
|
||||
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
|
||||
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control" required>
|
||||
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
|
||||
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
|
||||
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" class="form-control" required>
|
||||
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" placeholder="h" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
|
||||
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
|
||||
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" required>
|
||||
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" :placeholder="badgeConstants.defaultUpColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
|
||||
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
|
||||
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" required>
|
||||
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" :placeholder="badgeConstants.defaultDownColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
|
||||
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
|
||||
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" required>
|
||||
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" :placeholder="badgeConstants.defaultPendingColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
|
||||
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
|
||||
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" required>
|
||||
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
|
||||
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
|
||||
<input id="warnColor" v-model="badge.warnColor" type="number" min="0" class="form-control" required>
|
||||
<input id="warnColor" v-model="badge.warnColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
|
||||
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
|
||||
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" required>
|
||||
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireWarnDays">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
|
||||
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
|
||||
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" required>
|
||||
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -109,12 +109,16 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
|
||||
<input id="value" v-model="badge.value" type="text" class="form-control" required>
|
||||
<input id="value" v-model="badge.value" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 pt-3 d-flex justify-content-center">
|
||||
<img :src="badgeURL" :alt="$t('Badge Preview')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="push-url" class="form-label">{{ $t("Badge URL") }}</label>
|
||||
<CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" />
|
||||
<label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
|
||||
<CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +135,7 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
import { default as serverConfig } from "../../server/config.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -224,7 +229,8 @@ export default {
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
}
|
||||
},
|
||||
badgeConstants: serverConfig.badgeConstants,
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<div class="shadow-box mb-3" :style="boxStyle">
|
||||
<div class="list-header">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
<a v-if="searchText == ''" class="search-icon">
|
||||
<font-awesome-icon icon="search" />
|
||||
</a>
|
||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
<form>
|
||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
|
||||
</form>
|
||||
<div class="header-top">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper">
|
||||
<a v-if="searchText == ''" class="search-icon">
|
||||
<font-awesome-icon icon="search" />
|
||||
</a>
|
||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
<form>
|
||||
<input
|
||||
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-filter">
|
||||
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||
@@ -19,18 +27,23 @@
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
|
||||
<MonitorListItem
|
||||
v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item"
|
||||
:isSearch="searchText !== ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MonitorListItem from "../components/MonitorListItem.vue";
|
||||
import MonitorListFilter from "./MonitorListFilter.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MonitorListItem,
|
||||
MonitorListFilter,
|
||||
},
|
||||
props: {
|
||||
/** Should the scrollbar be shown */
|
||||
@@ -42,6 +55,11 @@ export default {
|
||||
return {
|
||||
searchText: "",
|
||||
windowTop: 0,
|
||||
filterState: {
|
||||
status: null,
|
||||
active: null,
|
||||
tags: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -72,8 +90,8 @@ export default {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
});
|
||||
} else {
|
||||
result = result.filter(monitor => monitor.parent === null);
|
||||
@@ -105,6 +123,27 @@ export default {
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
result.map(monitor => {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
});
|
||||
result = result.filter(monitor => this.filterState.status.includes(monitor.status));
|
||||
}
|
||||
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
result = result.filter(monitor => this.filterState.active.includes(monitor.active));
|
||||
}
|
||||
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
result = result.filter(monitor => {
|
||||
return monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||
.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
@@ -134,7 +173,14 @@ export default {
|
||||
/** Clear the search bar */
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update the MonitorList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
*/
|
||||
updateFilter(newFilter) {
|
||||
this.filterState = newFilter;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -159,8 +205,6 @@ export default {
|
||||
margin: -10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-header-bg;
|
||||
@@ -168,6 +212,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
@@ -216,5 +271,4 @@ export default {
|
||||
padding-left: 67px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
284
src/components/MonitorListFilter.vue
Normal file
284
src/components/MonitorListFilter.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="px-2 pt-2 d-flex">
|
||||
<button
|
||||
type="button"
|
||||
:title="$t('Clear current filters')"
|
||||
class="clear-filters-btn btn"
|
||||
:class="{ 'active': numFiltersActive > 0}"
|
||||
tabindex="0"
|
||||
:disabled="numFiltersActive === 0"
|
||||
@click="clearFilters"
|
||||
>
|
||||
<font-awesome-icon icon="stream" />
|
||||
<span v-if="numFiltersActive > 0" class="px-1 fw-bold">{{ numFiltersActive }}</span>
|
||||
<font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
|
||||
</button>
|
||||
<MonitorListFilterDropdown
|
||||
:filterActive="filterState.status?.length > 0"
|
||||
>
|
||||
<template #status>
|
||||
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
|
||||
<span v-else>
|
||||
{{ $t('Status') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #dropdown>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(1)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<Status :status="1" />
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.up }}
|
||||
<span v-if="filterState.status?.includes(1)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(0)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<Status :status="0" />
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.down }}
|
||||
<span v-if="filterState.status?.includes(0)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(2)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<Status :status="2" />
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.pending }}
|
||||
<span v-if="filterState.status?.includes(2)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(3)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<Status :status="3" />
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.maintenance }}
|
||||
<span v-if="filterState.status?.includes(3)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</MonitorListFilterDropdown>
|
||||
<MonitorListFilterDropdown :filterActive="filterState.active?.length > 0">
|
||||
<template #status>
|
||||
<span v-if="filterState.active?.length === 1">
|
||||
<span v-if="filterState.active[0]">{{ $t("Running") }}</span>
|
||||
<span v-else>{{ $t("filterActivePaused") }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("filterActive") }}
|
||||
</span>
|
||||
</template>
|
||||
<template #dropdown>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(true)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span>{{ $t("Running") }}</span>
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.active }}
|
||||
<span v-if="filterState.active?.includes(true)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span>{{ $t("filterActivePaused") }}</span>
|
||||
<span class="ps-3">
|
||||
{{ $root.stats.pause }}
|
||||
<span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</MonitorListFilterDropdown>
|
||||
<MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0">
|
||||
<template #status>
|
||||
<Tag
|
||||
v-if="filterState.tags?.length === 1"
|
||||
:item="tagsList.find(tag => tag.id === filterState.tags[0])"
|
||||
:size="'sm'"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ $t('Tags') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #dropdown>
|
||||
<li v-for="tag in tagsList" :key="tag.id">
|
||||
<div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span><Tag :item="tag" :size="'sm'" /></span>
|
||||
<span class="ps-3">
|
||||
{{ getTaggedMonitorCount(tag) }}
|
||||
<span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</MonitorListFilterDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MonitorListFilterDropdown from "./MonitorListFilterDropdown.vue";
|
||||
import Status from "./Status.vue";
|
||||
import Tag from "./Tag.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MonitorListFilterDropdown,
|
||||
Status,
|
||||
Tag,
|
||||
},
|
||||
props: {
|
||||
filterState: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: [ "updateFilter" ],
|
||||
data() {
|
||||
return {
|
||||
tagsList: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
numFiltersActive() {
|
||||
let num = 0;
|
||||
|
||||
Object.values(this.filterState).forEach(item => {
|
||||
if (item != null && item.length > 0) {
|
||||
num += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return num;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getExistingTags();
|
||||
},
|
||||
methods: {
|
||||
toggleStatusFilter(status) {
|
||||
let newFilter = {
|
||||
...this.filterState
|
||||
};
|
||||
|
||||
if (newFilter.status == null) {
|
||||
newFilter.status = [ status ];
|
||||
} else {
|
||||
if (newFilter.status.includes(status)) {
|
||||
newFilter.status = newFilter.status.filter(item => item !== status);
|
||||
} else {
|
||||
newFilter.status.push(status);
|
||||
}
|
||||
}
|
||||
this.$emit("updateFilter", newFilter);
|
||||
},
|
||||
toggleActiveFilter(active) {
|
||||
let newFilter = {
|
||||
...this.filterState
|
||||
};
|
||||
|
||||
if (newFilter.active == null) {
|
||||
newFilter.active = [ active ];
|
||||
} else {
|
||||
if (newFilter.active.includes(active)) {
|
||||
newFilter.active = newFilter.active.filter(item => item !== active);
|
||||
} else {
|
||||
newFilter.active.push(active);
|
||||
}
|
||||
}
|
||||
this.$emit("updateFilter", newFilter);
|
||||
},
|
||||
toggleTagFilter(tag) {
|
||||
let newFilter = {
|
||||
...this.filterState
|
||||
};
|
||||
|
||||
if (newFilter.tags == null) {
|
||||
newFilter.tags = [ tag.id ];
|
||||
} else {
|
||||
if (newFilter.tags.includes(tag.id)) {
|
||||
newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
|
||||
} else {
|
||||
newFilter.tags.push(tag.id);
|
||||
}
|
||||
}
|
||||
this.$emit("updateFilter", newFilter);
|
||||
},
|
||||
clearFilters() {
|
||||
this.$emit("updateFilter", {
|
||||
status: null,
|
||||
});
|
||||
},
|
||||
getExistingTags() {
|
||||
this.$root.getSocket().emit("getTags", (res) => {
|
||||
if (res.ok) {
|
||||
this.tagsList = res.tags;
|
||||
}
|
||||
});
|
||||
},
|
||||
getTaggedMonitorCount(tag) {
|
||||
return Object.values(this.$root.monitorList).filter(monitor => {
|
||||
return monitor.tags.find(monitorTag => monitorTag.tag_id === tag.id);
|
||||
}).length;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.clear-filters-btn {
|
||||
font-size: 0.8em;
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 16px;
|
||||
background-color: transparent;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
border: 1px solid $dark-font-color2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid $highlight;
|
||||
background-color: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
131
src/components/MonitorListFilterDropdown.vue
Normal file
131
src/components/MonitorListFilterDropdown.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
|
||||
<button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0">
|
||||
<div class="px-1 d-flex align-items-center">
|
||||
<slot name="status"></slot>
|
||||
</div>
|
||||
<span class="px-1">
|
||||
<font-awesome-icon icon="angle-down" />
|
||||
</span>
|
||||
</button>
|
||||
<ul class="filter-dropdown-menu" :class="{ 'open': open }">
|
||||
<slot name="dropdown"></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
filterActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
open: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleFocusOut(e) {
|
||||
if (e.relatedTarget != null && this.$el.contains(e.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.filter-dropdown-menu {
|
||||
z-index: 100;
|
||||
transition: all 0.2s;
|
||||
padding: 5px 0 !important;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
margin: 0;
|
||||
transform: translate(0, 36px);
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
visibility: hidden;
|
||||
list-style: none;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
background: white;
|
||||
|
||||
&.open {
|
||||
height: unset;
|
||||
visibility: inherit;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.dropdown-item:focus {
|
||||
background: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background: $dark-bg2;
|
||||
}
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-bg;
|
||||
color: $dark-font-color;
|
||||
border-color: $dark-border-color;
|
||||
|
||||
.dropdown-item {
|
||||
color: $dark-font-color;
|
||||
|
||||
&.active {
|
||||
color: $dark-font-color2;
|
||||
background-color: $highlight !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 25px;
|
||||
background-color: transparent;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
border: 1px solid $dark-font-color2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid $highlight;
|
||||
background-color: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-active {
|
||||
color: $highlight;
|
||||
}
|
||||
</style>
|
@@ -104,7 +104,7 @@ export default {
|
||||
// We must check if there are any elements in monitorList to
|
||||
// prevent undefined errors if it hasn't been loaded yet
|
||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||
}
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||
},
|
||||
|
@@ -164,6 +164,7 @@ export default {
|
||||
"SMSManager": "SmsManager (smsmanager.cz)",
|
||||
"WeCom": "WeCom (企业微信群机器人)",
|
||||
"ServerChan": "ServerChan (Server酱)",
|
||||
"smsc": "SMSC",
|
||||
};
|
||||
|
||||
// Sort by notification name
|
||||
|
@@ -150,7 +150,7 @@ export default {
|
||||
// We must check if there are any elements in monitorList to
|
||||
// prevent undefined errors if it hasn't been loaded yet
|
||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||
}
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||
},
|
||||
|
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
<button v-if="tag && tag.id !== null" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
|
43
src/components/notifications/SMSC.vue
Normal file
43
src/components/notifications/SMSC.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="smsc-login" class="form-label">{{ $t("API Username") }}</label>
|
||||
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
|
||||
<a href="https://smsc.kz/" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
<input id="smsc-login" v-model="$parent.notification.smscLogin" type="text" class="form-control" required>
|
||||
<label for="smsc-key" class="form-label">{{ $t("API Key") }}</label>
|
||||
<HiddenInput id="smsc-key" v-model="$parent.notification.smscPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-text">
|
||||
{{ $t("checkPrice", ['СМСЦ']) }}
|
||||
<a href="https://smsc.kz/tariffs/" target="_blank">https://smsc.kz/tariffs/</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smsc-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
|
||||
<input id="smsc-to-number" v-model="$parent.notification.smscToNumber" type="text" minlength="11" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smsc-sender-name" class="form-label">{{ $t("From Name/Number") }}</label>
|
||||
<input id="smsc-sender-name" v-model="$parent.notification.smscSenderName" type="text" minlength="1" maxlength="15" class="form-control">
|
||||
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smsc-platform" class="form-label">{{ $t("smscTranslit") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<select id="smsc-platform" v-model="$parent.notification.smscTranslit" class="form-select">
|
||||
<option value="0">{{ $t("Default") }}</option>
|
||||
<option value="1">Translit</option>
|
||||
<option value="2">MpaHc/Ium</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@@ -24,5 +24,13 @@
|
||||
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input id="slack-channel-notify" v-model="$parent.notification.slackchannelnotify" type="checkbox" class="form-check-input">
|
||||
<label for="slack-channel-notify" class="form-label">{{ $t("Notify Channel") }}</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ $t("aboutNotifyChannel") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -5,7 +5,18 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
|
||||
<label for="twilio-apikey-token" class="form-label">{{ $t("Api Key (optional)") }}</label>
|
||||
<input id="twilio-apikey-token" v-model="$parent.notification.twilioApiKey" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<p>
|
||||
The API key is optional but recommended. You can provide either Account SID and AuthToken
|
||||
from the may TwilioConsole page or Account SID and the pair of Api Key and Api Key secret
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token / Api Key Secret") }}</label>
|
||||
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
|
@@ -12,61 +12,97 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-content-type" class="form-label">{{
|
||||
$t("Content Type")
|
||||
<label for="webhook-request-body" class="form-label">{{
|
||||
$t("Request Body")
|
||||
}}</label>
|
||||
<select
|
||||
id="webhook-content-type"
|
||||
id="webhook-request-body"
|
||||
v-model="$parent.notification.webhookContentType"
|
||||
class="form-select"
|
||||
required
|
||||
>
|
||||
<option value="json">application/json</option>
|
||||
<option value="form-data">multipart/form-data</option>
|
||||
<option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
|
||||
<option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
|
||||
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
|
||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||
<template #multipart>"multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
<strong>json_decode($_POST['data'])</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div v-if="$parent.notification.webhookContentType == 'json'">
|
||||
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
|
||||
</div>
|
||||
<div v-if="$parent.notification.webhookContentType == 'form-data'">
|
||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||
<template #multipart>multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
<strong>json_decode($_POST['data'])</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div v-if="$parent.notification.webhookContentType == 'custom'">
|
||||
<i18n-t tag="p" keypath="webhookCustomBodyDesc">
|
||||
<template #msg>
|
||||
<code>msg</code>
|
||||
</template>
|
||||
<template #heartbeat>
|
||||
<code>heartbeatJSON</code>
|
||||
</template>
|
||||
<template #monitor>
|
||||
<code>monitorJSON</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-if="$parent.notification.webhookContentType == 'custom'"
|
||||
id="customBody"
|
||||
v-model="$parent.notification.webhookCustomBody"
|
||||
class="form-control"
|
||||
:placeholder="customBodyPlaceholder"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<i18n-t
|
||||
tag="label"
|
||||
class="form-label"
|
||||
for="additionalHeaders"
|
||||
keypath="webhookAdditionalHeadersTitle"
|
||||
>
|
||||
</i18n-t>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
|
||||
</div>
|
||||
<textarea
|
||||
v-if="showAdditionalHeadersField"
|
||||
id="additionalHeaders"
|
||||
v-model="$parent.notification.webhookAdditionalHeaders"
|
||||
class="form-control"
|
||||
:placeholder="headersPlaceholder"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
headersPlaceholder() {
|
||||
return this.$t("Example:", [
|
||||
`
|
||||
{
|
||||
"HeaderName": "HeaderValue"
|
||||
"Authorization": "Authorization Token"
|
||||
}`,
|
||||
]);
|
||||
},
|
||||
customBodyPlaceholder() {
|
||||
return `Example:
|
||||
{
|
||||
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
|
||||
"Body": "{{ msg }}"
|
||||
}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
|
||||
import Apprise from "./Apprise.vue";
|
||||
import Bark from "./Bark.vue";
|
||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||
import SMSC from "./SMSC.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
import Discord from "./Discord.vue";
|
||||
import Feishu from "./Feishu.vue";
|
||||
@@ -61,6 +62,7 @@ const NotificationFormList = {
|
||||
"apprise": Apprise,
|
||||
"Bark": Bark,
|
||||
"clicksendsms": ClickSendSMS,
|
||||
"smsc": SMSC,
|
||||
"DingDing": DingDing,
|
||||
"discord": Discord,
|
||||
"Feishu": Feishu,
|
||||
|
@@ -190,6 +190,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chrome Executable -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">
|
||||
{{ $t("chromeExecutable") }}
|
||||
</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
id="primaryBaseURL"
|
||||
v-model="settings.chromeExecutable"
|
||||
class="form-control"
|
||||
name="primaryBaseURL"
|
||||
:placeholder="$t('chromeExecutableAutoDetect')"
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" @click="testChrome">
|
||||
{{ $t("Test") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("chromeExecutableDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
@@ -241,6 +265,12 @@ export default {
|
||||
autoGetPrimaryBaseURL() {
|
||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||
},
|
||||
|
||||
testChrome() {
|
||||
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -51,6 +51,9 @@
|
||||
"Ping": "Ping",
|
||||
"Monitor Type": "Monitor Type",
|
||||
"Keyword": "Keyword",
|
||||
"Invert Keyword": "Invert Keyword",
|
||||
"Expected Value": "Expected Value",
|
||||
"Json Query": "Json Query",
|
||||
"Friendly Name": "Friendly Name",
|
||||
"URL": "URL",
|
||||
"Hostname": "Hostname",
|
||||
@@ -152,6 +155,8 @@
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
"filterActive": "Active",
|
||||
"filterActivePaused": "Paused",
|
||||
"Active": "Active",
|
||||
"Inactive": "Inactive",
|
||||
"Token": "Token",
|
||||
@@ -195,8 +200,11 @@
|
||||
"Content Type": "Content Type",
|
||||
"webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
|
||||
"webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
|
||||
"webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
|
||||
"webhookAdditionalHeadersTitle": "Additional Headers",
|
||||
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook.",
|
||||
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
|
||||
"webhookBodyPresetOption": "Preset - {0}",
|
||||
"webhookBodyCustomOption": "Custom Body",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"Application Token": "Application Token",
|
||||
"Server URL": "Server URL",
|
||||
@@ -435,6 +443,9 @@
|
||||
"Enable DNS Cache": "Enable DNS Cache",
|
||||
"Enable": "Enable",
|
||||
"Disable": "Disable",
|
||||
"chromeExecutable": "Chrome/Chromium Executable",
|
||||
"chromeExecutableAutoDetect": "Auto Detect",
|
||||
"chromeExecutableDescription": "For Docker users, if Chromium is not yet installed, it may take a few minutes to install and display the test result. It takes 1GB of disk space.",
|
||||
"dnsCacheDescription": "It may be not working in some IPv6 environments, disable it if you encounter any issues.",
|
||||
"Single Maintenance Window": "Single Maintenance Window",
|
||||
"Maintenance Time Window of a Day": "Maintenance Time Window of a Day",
|
||||
@@ -515,6 +526,8 @@
|
||||
"passwordNotMatchMsg": "The repeat password does not match.",
|
||||
"notificationDescription": "Notifications must be assigned to a monitor to function.",
|
||||
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
|
||||
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
|
||||
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out <a href='https://jsonata.org/'>jsonata.org</a> for the documentation about the query language. A playground can be found <a href='https://try.jsonata.org/'>here</a>.",
|
||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||
"backupDescription2": "Note: history and event data is not included.",
|
||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||
@@ -629,6 +642,8 @@
|
||||
"matrixDesc1": "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc2": "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
|
||||
"Channel Name": "Channel Name",
|
||||
"Notify Channel": "Notify Channel",
|
||||
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||
"Icon Emoji": "Icon Emoji",
|
||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||
@@ -722,7 +737,8 @@
|
||||
"ntfyAuthenticationMethod": "Authentication Method",
|
||||
"ntfyUsernameAndPassword": "Username and Password",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"twilioApiKey": "Api Key (optional)",
|
||||
"twilioAuthToken": "Auth Token / Api Key Secret",
|
||||
"twilioFromNumber": "From Number",
|
||||
"twilioToNumber": "To Number",
|
||||
"Monitor Setting": "{0}'s Monitor Setting",
|
||||
@@ -731,13 +747,14 @@
|
||||
"Open Badge Generator": "Open Badge Generator",
|
||||
"Badge Generator": "{0}'s Badge Generator",
|
||||
"Badge Type": "Badge Type",
|
||||
"Badge Duration": "Badge Duration",
|
||||
"Badge Duration (in hours)": "Badge Duration (in hours)",
|
||||
"Badge Label": "Badge Label",
|
||||
"Badge Prefix": "Badge Prefix",
|
||||
"Badge Suffix": "Badge Suffix",
|
||||
"Badge Prefix": "Badge Value Prefix",
|
||||
"Badge Suffix": "Badge Value Suffix",
|
||||
"Badge Label Color": "Badge Label Color",
|
||||
"Badge Color": "Badge Color",
|
||||
"Badge Label Prefix": "Badge Label Prefix",
|
||||
"Badge Preview": "Badge Preview",
|
||||
"Badge Label Suffix": "Badge Label Suffix",
|
||||
"Badge Up Color": "Badge Up Color",
|
||||
"Badge Down Color": "Badge Down Color",
|
||||
@@ -751,6 +768,21 @@
|
||||
"Badge URL": "Badge URL",
|
||||
"Group": "Group",
|
||||
"Monitor Group": "Monitor Group",
|
||||
"Kafka Brokers": "Kafka Brokers",
|
||||
"Enter the list of brokers": "Enter the list of brokers",
|
||||
"Press Enter to add broker": "Press Enter to add broker",
|
||||
"Kafka Topic Name": "Kafka Topic Name",
|
||||
"Kafka Producer Message": "Kafka Producer Message",
|
||||
"Enable Kafka SSL": "Enable Kafka SSL",
|
||||
"Enable Kafka Producer Auto Topic Creation": "Enable Kafka Producer Auto Topic Creation",
|
||||
"Kafka SASL Options": "Kafka SASL Options",
|
||||
"Mechanism": "Mechanism",
|
||||
"Pick a SASL Mechanism...": "Pick a SASL Mechanism...",
|
||||
"Authorization Identity": "Authorization Identity",
|
||||
"AccessKey Id": "AccessKey Id",
|
||||
"Secret AccessKey": "Secret AccessKey",
|
||||
"Session Token": "Session Token",
|
||||
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
|
||||
"Close": "Close"
|
||||
"Close": "Close",
|
||||
"Request Body": "Request Body"
|
||||
}
|
||||
|
@@ -139,6 +139,8 @@
|
||||
"Disable 2FA": "關閉 2FA",
|
||||
"2FA Settings": "2FA 設定",
|
||||
"Two Factor Authentication": "雙重認證",
|
||||
"filterActive": "執行狀態",
|
||||
"filterActivePaused": "已暫停",
|
||||
"Active": "生效",
|
||||
"Inactive": "未生效",
|
||||
"Token": "Token",
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import axios from "axios";
|
||||
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend";
|
||||
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
|
||||
// change the axios base url for development
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
if (env === "development" && isDevContainer()) {
|
||||
axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname();
|
||||
} else if (env === "development" || localStorage.dev === "dev") {
|
||||
axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import jwtDecode from "jwt-decode";
|
||||
import Favico from "favico.js";
|
||||
import dayjs from "dayjs";
|
||||
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
|
||||
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js";
|
||||
const toast = useToast();
|
||||
|
||||
let socket;
|
||||
@@ -93,7 +94,9 @@ export default {
|
||||
|
||||
let wsHost;
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
if (env === "development" && isDevContainer()) {
|
||||
wsHost = protocol + getDevContainerServerHostname();
|
||||
} else if (env === "development" || localStorage.dev === "dev") {
|
||||
wsHost = protocol + location.hostname + ":3001";
|
||||
} else {
|
||||
wsHost = protocol + location.host;
|
||||
@@ -693,9 +696,11 @@ export default {
|
||||
|
||||
stats() {
|
||||
let result = {
|
||||
active: 0,
|
||||
up: 0,
|
||||
down: 0,
|
||||
maintenance: 0,
|
||||
pending: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
@@ -707,12 +712,13 @@ export default {
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
result.active++;
|
||||
if (beat.status === UP) {
|
||||
result.up++;
|
||||
} else if (beat.status === DOWN) {
|
||||
result.down++;
|
||||
} else if (beat.status === PENDING) {
|
||||
result.up++;
|
||||
result.pending++;
|
||||
} else if (beat.status === MAINTENANCE) {
|
||||
result.maintenance++;
|
||||
} else {
|
||||
|
@@ -8,12 +8,20 @@
|
||||
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br>
|
||||
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
|
||||
<span>{{ $t("Keyword") }}: </span>
|
||||
<span class="keyword">{{ monitor.keyword }}</span>
|
||||
<span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> ↧</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'json-query'">
|
||||
<br>
|
||||
<span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span>
|
||||
<br>
|
||||
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
|
||||
<br>
|
||||
@@ -68,6 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="shadow-box big-padding text-center stats">
|
||||
<div class="row">
|
||||
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||
@@ -131,6 +140,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screenshot -->
|
||||
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<img :src="screenshotURL" alt style="width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box table-shadow-box">
|
||||
<div class="dropdown dropdown-clear-data">
|
||||
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
@@ -217,6 +235,7 @@ import Tag from "../components/Tag.vue";
|
||||
import CertificateInfo from "../components/CertificateInfo.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
import { URL } from "whatwg-url";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -242,6 +261,7 @@ export default {
|
||||
hideCount: true,
|
||||
chunksNavigation: "scroll",
|
||||
},
|
||||
cacheTime: Date.now(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -251,6 +271,10 @@ export default {
|
||||
},
|
||||
|
||||
lastHeartBeat() {
|
||||
// Also trigger screenshot refresh here
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
this.cacheTime = Date.now();
|
||||
|
||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||
return this.$root.lastHeartbeatList[this.monitor.id];
|
||||
}
|
||||
@@ -325,11 +349,16 @@ export default {
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||
},
|
||||
|
||||
screenshotURL() {
|
||||
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
getResBaseURL,
|
||||
/** Request a test notification be sent for this monitor */
|
||||
testNotification() {
|
||||
this.$root.getSocket().emit("testNotification", this.monitor.id);
|
||||
@@ -411,7 +440,7 @@ export default {
|
||||
translationPrefix = "Avg. ";
|
||||
}
|
||||
|
||||
if (this.monitor.type === "http" || this.monitor.type === "keyword") {
|
||||
if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") {
|
||||
return this.$t(translationPrefix + "Response");
|
||||
}
|
||||
|
||||
@@ -561,6 +590,10 @@ table {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.keyword-inverted {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.dropdown-clear-data {
|
||||
ul {
|
||||
background-color: $dark-bg;
|
||||
|
@@ -27,6 +27,9 @@
|
||||
<option value="keyword">
|
||||
HTTP(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="json-query">
|
||||
HTTP(s) - {{ $t("Json Query") }}
|
||||
</option>
|
||||
<option value="grpc-keyword">
|
||||
gRPC(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
@@ -36,6 +39,10 @@
|
||||
<option value="docker">
|
||||
{{ $t("Docker Container") }}
|
||||
</option>
|
||||
|
||||
<option value="real-browser">
|
||||
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup :label="$t('Passive Monitor Type')">
|
||||
@@ -54,6 +61,9 @@
|
||||
<option value="mqtt">
|
||||
MQTT
|
||||
</option>
|
||||
<option value="kafka-producer">
|
||||
Kafka Producer
|
||||
</option>
|
||||
<option value="sqlserver">
|
||||
Microsoft SQL Server
|
||||
</option>
|
||||
@@ -73,16 +83,6 @@
|
||||
Redis
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<!--
|
||||
Hidden for now: Reason refer to Setting.vue
|
||||
<optgroup :label="$t('Custom Monitor Type')">
|
||||
<option value="browser">
|
||||
(Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
-->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
|
||||
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
@@ -133,6 +133,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invert keyword -->
|
||||
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3 form-check">
|
||||
<input id="invert-keyword" v-model="monitor.invertKeyword" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="invert-keyword">
|
||||
{{ $t("Invert Keyword") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("invertKeywordDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Json Query -->
|
||||
<div v-if="monitor.type === 'json-query'" class="my-3">
|
||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="form-text" v-html="$t('jsonQueryDescription')">
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Game -->
|
||||
<!-- GameDig only -->
|
||||
<div v-if="monitor.type === 'gamedig'" class="my-3">
|
||||
@@ -144,6 +169,57 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template v-if="monitor.type === 'kafka-producer'">
|
||||
<!-- Kafka Brokers List -->
|
||||
<div class="my-3">
|
||||
<label for="kafkaProducerBrokers" class="form-label">{{ $t("Kafka Brokers") }}</label>
|
||||
<VueMultiselect
|
||||
id="kafkaProducerBrokers"
|
||||
v-model="monitor.kafkaProducerBrokers"
|
||||
:multiple="true"
|
||||
:options="[]"
|
||||
:placeholder="$t('Enter the list of brokers')"
|
||||
:tag-placeholder="$t('Press Enter to add broker')"
|
||||
:max-height="500"
|
||||
:taggable="true"
|
||||
:show-no-options="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="false"
|
||||
:preselect-first="false"
|
||||
@tag="addKafkaProducerBroker"
|
||||
></VueMultiselect>
|
||||
</div>
|
||||
|
||||
<!-- Kafka Topic Name -->
|
||||
<div class="my-3">
|
||||
<label for="kafkaProducerTopic" class="form-label">{{ $t("Kafka Topic Name") }}</label>
|
||||
<input id="kafkaProducerTopic" v-model="monitor.kafkaProducerTopic" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Kafka Producer Message -->
|
||||
<div class="my-3">
|
||||
<label for="kafkaProducerMessage" class="form-label">{{ $t("Kafka Producer Message") }}</label>
|
||||
<input id="kafkaProducerMessage" v-model="monitor.kafkaProducerMessage" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Kafka SSL -->
|
||||
<div class="my-3 form-check">
|
||||
<input id="kafkaProducerSsl" v-model="monitor.kafkaProducerSsl" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="kafkaProducerSsl">
|
||||
{{ $t("Enable Kafka SSL") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Kafka SSL -->
|
||||
<div class="my-3 form-check">
|
||||
<input id="kafkaProducerAllowAutoTopicCreation" v-model="monitor.kafkaProducerAllowAutoTopicCreation" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="kafkaProducerAllowAutoTopicCreation">
|
||||
{{ $t("Enable Kafka Producer Auto Topic Creation") }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Hostname -->
|
||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
@@ -362,7 +438,7 @@
|
||||
|
||||
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
|
||||
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="expiry-notification">
|
||||
{{ $t("Certificate Expiry Notification") }}
|
||||
@@ -371,7 +447,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
{{ $t("ignoreTLSError") }}
|
||||
@@ -463,7 +539,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Proxies -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'">
|
||||
<h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
|
||||
<p v-if="$root.proxyList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
@@ -490,8 +566,58 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Kafka SASL Options -->
|
||||
<!-- Kafka Producer only -->
|
||||
<template v-if="monitor.type === 'kafka-producer'">
|
||||
<h2 class="mt-5 mb-2">{{ $t("Kafka SASL Options") }}</h2>
|
||||
<div class="my-3">
|
||||
<label class="form-label" for="kafkaProducerSaslMechanism">
|
||||
{{ $t("Mechanism") }}
|
||||
</label>
|
||||
<VueMultiselect
|
||||
id="kafkaProducerSaslMechanism"
|
||||
v-model="monitor.kafkaProducerSaslOptions.mechanism"
|
||||
:options="kafkaSaslMechanismOptions"
|
||||
:multiple="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="false"
|
||||
:placeholder="$t('Pick a SASL Mechanism...')"
|
||||
:preselect-first="false"
|
||||
:max-height="500"
|
||||
:allow-empty="false"
|
||||
:taggable="false"
|
||||
></VueMultiselect>
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'None'">
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslUsername" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="kafkaProducerSaslUsername" v-model="monitor.kafkaProducerSaslOptions.username" type="text" autocomplete="kafkaProducerSaslUsername" class="form-control">
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslPassword" class="form-label">{{ $t("Password") }}</label>
|
||||
<input id="kafkaProducerSaslPassword" v-model="monitor.kafkaProducerSaslOptions.password" type="password" autocomplete="kafkaProducerSaslPassword" class="form-control">
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslAuthorizationIdentity" class="form-label">{{ $t("Authorization Identity") }}</label>
|
||||
<input id="kafkaProducerSaslAuthorizationIdentity" v-model="monitor.kafkaProducerSaslOptions.authorizationIdentity" type="text" autocomplete="kafkaProducerSaslAuthorizationIdentity" class="form-control" required>
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslAccessKeyId" class="form-label">{{ $t("AccessKey Id") }}</label>
|
||||
<input id="kafkaProducerSaslAccessKeyId" v-model="monitor.kafkaProducerSaslOptions.accessKeyId" type="text" autocomplete="kafkaProducerSaslAccessKeyId" class="form-control" required>
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslSecretAccessKey" class="form-label">{{ $t("Secret AccessKey") }}</label>
|
||||
<input id="kafkaProducerSaslSecretAccessKey" v-model="monitor.kafkaProducerSaslOptions.secretAccessKey" type="password" autocomplete="kafkaProducerSaslSecretAccessKey" class="form-control" required>
|
||||
</div>
|
||||
<div v-if="monitor.kafkaProducerSaslOptions.mechanism === 'aws'" class="my-3">
|
||||
<label for="kafkaProducerSaslSessionToken" class="form-label">{{ $t("Session Token") }}</label>
|
||||
<input id="kafkaProducerSaslSessionToken" v-model="monitor.kafkaProducerSaslOptions.sessionToken" type="password" autocomplete="kafkaProducerSaslSessionToken" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- HTTP Options -->
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' ">
|
||||
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
|
||||
|
||||
<!-- Method -->
|
||||
@@ -702,6 +828,7 @@ export default {
|
||||
},
|
||||
acceptedStatusCodeOptions: [],
|
||||
dnsresolvetypeOptions: [],
|
||||
kafkaSaslMechanismOptions: [],
|
||||
ipOrHostnameRegexPattern: hostNameRegexPattern(),
|
||||
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
|
||||
gameList: null,
|
||||
@@ -965,12 +1092,21 @@ message HealthCheckResponse {
|
||||
"TXT",
|
||||
];
|
||||
|
||||
let kafkaSaslMechanismOptions = [
|
||||
"None",
|
||||
"plain",
|
||||
"scram-sha-256",
|
||||
"scram-sha-512",
|
||||
"aws",
|
||||
];
|
||||
|
||||
for (let i = 100; i <= 999; i++) {
|
||||
acceptedStatusCodeOptions.push(i.toString());
|
||||
}
|
||||
|
||||
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
|
||||
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
|
||||
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
||||
},
|
||||
methods: {
|
||||
/** Initialize the edit monitor form */
|
||||
@@ -1004,7 +1140,11 @@ message HealthCheckResponse {
|
||||
mqttTopic: "",
|
||||
mqttSuccessMessage: "",
|
||||
authMethod: null,
|
||||
httpBodyEncoding: "json"
|
||||
httpBodyEncoding: "json",
|
||||
kafkaProducerBrokers: [],
|
||||
kafkaProducerSaslOptions: {
|
||||
mechanism: "None",
|
||||
},
|
||||
};
|
||||
|
||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||
@@ -1045,6 +1185,7 @@ message HealthCheckResponse {
|
||||
this.monitor.childrenIDs = undefined;
|
||||
this.monitor.forceInactive = undefined;
|
||||
this.monitor.pathName = undefined;
|
||||
this.monitor.screenshot = undefined;
|
||||
|
||||
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
|
||||
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => {
|
||||
@@ -1071,6 +1212,10 @@ message HealthCheckResponse {
|
||||
|
||||
},
|
||||
|
||||
addKafkaProducerBroker(newBroker) {
|
||||
this.monitor.kafkaProducerBrokers.push(newBroker);
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form input
|
||||
* @returns {boolean} Is the form input valid?
|
||||
@@ -1113,7 +1258,7 @@ message HealthCheckResponse {
|
||||
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
|
||||
}
|
||||
|
||||
if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") {
|
||||
if (this.monitor.type && this.monitor.type !== "http" && (this.monitor.type !== "keyword" || this.monitor.type !== "json-query")) {
|
||||
this.monitor.httpBodyEncoding = null;
|
||||
}
|
||||
|
||||
|
@@ -325,7 +325,7 @@
|
||||
</p>
|
||||
|
||||
<div class="refresh-info mb-2">
|
||||
<div>{{ $t("Last Updated") }}: <date-time :value="lastUpdateTime" /></div>
|
||||
<div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div>
|
||||
<div>{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -360,7 +360,6 @@ import DOMPurify from "dompurify";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||
import DateTime from "../components/Datetime.vue";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
|
||||
import Tag from "../components/Tag.vue";
|
||||
@@ -386,7 +385,6 @@ export default {
|
||||
Confirm,
|
||||
PrismEditor,
|
||||
MaintenanceTime,
|
||||
DateTime,
|
||||
Tag,
|
||||
VueMultiselect
|
||||
},
|
||||
@@ -583,6 +581,10 @@ export default {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
lastUpdateTimeDisplay() {
|
||||
return this.$root.datetime(this.lastUpdateTime);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
|
@@ -72,13 +72,32 @@ export function setPageLocale() {
|
||||
*/
|
||||
export function getResBaseURL() {
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
if (env === "development" && isDevContainer()) {
|
||||
return location.protocol + "//" + getDevContainerServerHostname();
|
||||
} else if (env === "development" || localStorage.dev === "dev") {
|
||||
return location.protocol + "//" + location.hostname + ":3001";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function isDevContainer() {
|
||||
// eslint-disable-next-line no-undef
|
||||
return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports GitHub Codespaces only currently
|
||||
*/
|
||||
export function getDevContainerServerHostname() {
|
||||
if (!isDevContainer()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} mqtt wheather or not the regex should take into account the fact that it is an mqtt uri
|
||||
|
@@ -306,6 +306,16 @@ describe("Test uptimeKumaServer.getClientIP()", () => {
|
||||
ip = await server.getClientIP(fakeSocket);
|
||||
expect(ip).toBe("203.0.113.195");
|
||||
|
||||
fakeSocket.client.conn.remoteAddress = "2001:db8::1";
|
||||
fakeSocket.client.conn.request.headers = {};
|
||||
ip = await server.getClientIP(fakeSocket);
|
||||
expect(ip).toBe("2001:db8::1");
|
||||
|
||||
fakeSocket.client.conn.remoteAddress = "::ffff:127.0.0.1";
|
||||
fakeSocket.client.conn.request.headers = {};
|
||||
ip = await server.getClientIP(fakeSocket);
|
||||
expect(ip).toBe("127.0.0.1");
|
||||
|
||||
await Database.close();
|
||||
}, 120000);
|
||||
});
|
||||
|
Reference in New Issue
Block a user