mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-01 19:32:34 +02:00
Merge remote-tracking branch 'upstream/master'
# Conflicts: # README.md # dockerfile # package-lock.json # package.json # server/notification.js # server/server.js # src/assets/vars.scss # src/components/NotificationDialog.vue # src/layouts/Layout.vue # src/mixins/socket.js # src/pages/Dashboard.vue # src/pages/EditMonitor.vue
This commit is contained in:
commit
736ddf4f03
35 changed files with 1071 additions and 498 deletions
|
@ -2,3 +2,13 @@
|
||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/data/kuma.db
|
/data/kuma.db
|
||||||
|
/.do
|
||||||
|
**/.dockerignore
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
.editorconfig
|
||||||
|
.vscode
|
||||||
|
|
10
.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: ⚠ Please go to "Discussions" Tab if you want to ask or share something
|
||||||
|
about: BUG REPORT ONLY HERE
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
BUG REPORT ONLY HERE
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- Uptime Kuma Version:
|
||||||
|
- Using Docker?: Yes/No
|
||||||
|
- OS:
|
||||||
|
- Browser:
|
||||||
|
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,3 +7,4 @@ dist-ssr
|
||||||
|
|
||||||
/data
|
/data
|
||||||
!/data/.gitkeep
|
!/data/.gitkeep
|
||||||
|
.vscode
|
BIN
db/kuma.db
BIN
db/kuma.db
Binary file not shown.
37
db/patch1.sql
Normal file
37
db/patch1.sql
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
|
||||||
|
-- SQL Generated by Intellij Idea
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER
|
||||||
|
references user
|
||||||
|
on update cascade on delete set null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME,
|
||||||
|
keyword VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=on;
|
37
db/patch3.sql
Normal file
37
db/patch3.sql
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
-- Add maxretries column to monitor
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER
|
||||||
|
references user
|
||||||
|
on update cascade on delete set null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME,
|
||||||
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=on;
|
19
extra/healthcheck.js
Normal file
19
extra/healthcheck.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
var http = require("http");
|
||||||
|
var options = {
|
||||||
|
host: "localhost",
|
||||||
|
port: "3001",
|
||||||
|
timeout: 2000,
|
||||||
|
};
|
||||||
|
var request = http.request(options, (res) => {
|
||||||
|
console.log(`STATUS: ${res.statusCode}`);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.on("error", function (err) {
|
||||||
|
console.log("ERROR");
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
request.end();
|
40
extra/mark-as-nightly.js
Normal file
40
extra/mark-as-nightly.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* String.prototype.replaceAll() polyfill
|
||||||
|
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||||
|
* @author Chris Ferdinandi
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
if (!String.prototype.replaceAll) {
|
||||||
|
String.prototype.replaceAll = function(str, newStr){
|
||||||
|
|
||||||
|
// If a regex pattern
|
||||||
|
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
||||||
|
return this.replace(str, newStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a string
|
||||||
|
return this.replace(new RegExp(str, 'g'), newStr);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = require('../package.json');
|
||||||
|
const fs = require("fs");
|
||||||
|
const oldVersion = pkg.version
|
||||||
|
const newVersion = oldVersion + "-nightly"
|
||||||
|
|
||||||
|
console.log("Old Version: " + oldVersion)
|
||||||
|
console.log("New Version: " + newVersion)
|
||||||
|
|
||||||
|
if (newVersion) {
|
||||||
|
// Process package.json
|
||||||
|
pkg.version = newVersion
|
||||||
|
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
|
||||||
|
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
||||||
|
|
||||||
|
// Process README.md
|
||||||
|
if (fs.existsSync("README.md")) {
|
||||||
|
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#5cdd8b" />
|
||||||
|
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||||
<title>Uptime Kuma</title>
|
<title>Uptime Kuma</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
90
package.json
90
package.json
|
@ -1,65 +1,55 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.2.0",
|
"version": "1.0.6",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"update": "",
|
"update": "",
|
||||||
"release": "release-it",
|
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.1 . --push",
|
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.6 --target release . --push",
|
||||||
"setup": "git checkout 1.0.1 && npm install && npm run build"
|
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
|
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||||
|
"setup": "git checkout 1.0.6 && npm install && npm run build",
|
||||||
|
"version-global-replace": "node extra/version-global-replace.js",
|
||||||
|
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
"args-parser": "1.3.0",
|
"args-parser": "^1.3.0",
|
||||||
"axios": "0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bootstrap": "5.0.2",
|
"bootstrap": "^5.0.2",
|
||||||
"dayjs": "1.10.6",
|
"command-exists": "^1.2.9",
|
||||||
"express": "4.17.1",
|
"dayjs": "^1.10.6",
|
||||||
"form-data": "4.0.0",
|
"express": "^4.17.1",
|
||||||
"jsonwebtoken": "8.5.1",
|
"form-data": "^4.0.0",
|
||||||
"nodemailer": "6.6.3",
|
"http-graceful-shutdown": "^3.1.2",
|
||||||
"password-hash": "1.2.2",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"nodemailer": "^6.6.3",
|
||||||
|
"password-hash": "^1.2.2",
|
||||||
"redbean-node": "0.0.20",
|
"redbean-node": "0.0.20",
|
||||||
"socket.io": "4.1.3",
|
"socket.io": "^4.1.3",
|
||||||
"socket.io-client": "4.1.3",
|
"socket.io-client": "^4.1.3",
|
||||||
"tcp-ping": "0.1.1",
|
"sqlite3": "^5.0.2",
|
||||||
"vue": "3.1.4",
|
"tcp-ping": "^0.1.1",
|
||||||
"vue-confirm-dialog": "1.0.2",
|
"v-pagination-3": "^0.1.6",
|
||||||
"vue-router": "4.0.10",
|
"vue": "^3.0.5",
|
||||||
"vue-toastification": "2.0.0-rc.1"
|
"vue-confirm-dialog": "^1.0.2",
|
||||||
|
"vue-router": "^4.0.10",
|
||||||
|
"vue-toastification": "^2.0.0-rc.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-legacy": "1.4.4",
|
"@vitejs/plugin-legacy": "^1.4.4",
|
||||||
"@vitejs/plugin-vue": "1.2.5",
|
"@vitejs/plugin-vue": "^1.2.5",
|
||||||
"@vue/compiler-sfc": "3.1.4",
|
"@vue/compiler-sfc": "^3.1.5",
|
||||||
"auto-changelog": "2.3.0",
|
"core-js": "^3.15.2",
|
||||||
"core-js": "3.15.2",
|
"sass": "^1.35.2",
|
||||||
"release-it": "14.10.0",
|
"vite": "^2.4.2"
|
||||||
"sass": "1.35.2",
|
|
||||||
"vite": "2.4.2"
|
|
||||||
},
|
|
||||||
"release-it": {
|
|
||||||
"git": {
|
|
||||||
"commit": true,
|
|
||||||
"requireCleanWorkingDir": false,
|
|
||||||
"commitMessage": "🚀RELEASE v${version}",
|
|
||||||
"push": false,
|
|
||||||
"tag": true,
|
|
||||||
"tagName": "v${version}",
|
|
||||||
"tagAnnotation": "v${version}"
|
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"publish": false
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"after:bump": "auto-changelog --commit-limit false -p -u --hide-credit && git add CHANGELOG.md"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"volta": {
|
|
||||||
"node": "16.4.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 2.5 KiB |
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
119
server/database.js
Normal file
119
server/database.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const {sleep} = require("./util");
|
||||||
|
const {R} = require("redbean-node");
|
||||||
|
const {setSetting, setting} = require("./util-server");
|
||||||
|
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
|
||||||
|
static templatePath = "./db/kuma.db"
|
||||||
|
static path = './data/kuma.db';
|
||||||
|
static latestVersion = 1;
|
||||||
|
static noReject = true;
|
||||||
|
|
||||||
|
static async patch() {
|
||||||
|
let version = parseInt(await setting("database_version"));
|
||||||
|
|
||||||
|
if (! version) {
|
||||||
|
version = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("Your database version: " + version);
|
||||||
|
console.info("Latest database version: " + this.latestVersion);
|
||||||
|
|
||||||
|
if (version === this.latestVersion) {
|
||||||
|
console.info("Database no need to patch");
|
||||||
|
} else {
|
||||||
|
console.info("Database patch is needed")
|
||||||
|
|
||||||
|
console.info("Backup the db")
|
||||||
|
const backupPath = "./data/kuma.db.bak" + version;
|
||||||
|
fs.copyFileSync(Database.path, backupPath);
|
||||||
|
|
||||||
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
|
try {
|
||||||
|
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||||
|
const sqlFile = `./db/patch${i}.sql`;
|
||||||
|
console.info(`Patching ${sqlFile}`);
|
||||||
|
await Database.importSQLFile(sqlFile);
|
||||||
|
console.info(`Patched ${sqlFile}`);
|
||||||
|
await setSetting("database_version", i);
|
||||||
|
}
|
||||||
|
console.log("Database Patched Successfully");
|
||||||
|
} catch (ex) {
|
||||||
|
await Database.close();
|
||||||
|
console.error("Patch db failed!!! Restoring the backup")
|
||||||
|
fs.copyFileSync(backupPath, Database.path);
|
||||||
|
console.error(ex)
|
||||||
|
|
||||||
|
console.error("Start Uptime-Kuma failed due to patch db failed")
|
||||||
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||||
|
* @param filename
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async importSQLFile(filename) {
|
||||||
|
|
||||||
|
await R.getCell("SELECT 1");
|
||||||
|
|
||||||
|
let text = fs.readFileSync(filename).toString();
|
||||||
|
|
||||||
|
// Remove all comments (--)
|
||||||
|
let lines = text.split("\n");
|
||||||
|
lines = lines.filter((line) => {
|
||||||
|
return ! line.startsWith("--")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Split statements by semicolon
|
||||||
|
// Filter out empty line
|
||||||
|
text = lines.join("\n")
|
||||||
|
|
||||||
|
let statements = text.split(";")
|
||||||
|
.map((statement) => {
|
||||||
|
return statement.trim();
|
||||||
|
})
|
||||||
|
.filter((statement) => {
|
||||||
|
return statement !== "";
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let statement of statements) {
|
||||||
|
await R.exec(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async close() {
|
||||||
|
const listener = (reason, p) => {
|
||||||
|
Database.noReject = false;
|
||||||
|
};
|
||||||
|
process.addListener('unhandledRejection', listener);
|
||||||
|
|
||||||
|
console.log("Closing DB")
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
Database.noReject = true;
|
||||||
|
await R.close()
|
||||||
|
await sleep(2000)
|
||||||
|
|
||||||
|
if (Database.noReject) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
console.log("Waiting to close the db")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("SQLite closed")
|
||||||
|
|
||||||
|
process.removeListener('unhandledRejection', listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Database;
|
|
@ -3,8 +3,6 @@ const utc = require('dayjs/plugin/utc')
|
||||||
var timezone = require('dayjs/plugin/timezone')
|
var timezone = require('dayjs/plugin/timezone')
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
const axios = require("axios");
|
|
||||||
const {R} = require("redbean-node");
|
|
||||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
const {BeanModel} = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ var timezone = require('dayjs/plugin/timezone')
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const {UP, DOWN, PENDING} = require("../util");
|
||||||
const {tcping, ping} = require("../util-server");
|
const {tcping, ping} = require("../util-server");
|
||||||
const {R} = require("redbean-node");
|
const {R} = require("redbean-node");
|
||||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
const {BeanModel} = require("redbean-node/dist/bean-model");
|
||||||
|
@ -16,7 +17,6 @@ const {Notification} = require("../notification")
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
let notificationIDList = {};
|
let notificationIDList = {};
|
||||||
|
@ -35,6 +35,7 @@ class Monitor extends BeanModel {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
|
maxretries: this.maxretries,
|
||||||
weight: this.weight,
|
weight: this.weight,
|
||||||
active: this.active,
|
active: this.active,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
@ -46,9 +47,9 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
start(io) {
|
start(io) {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
console.log(`Monitor ${this.id}: Heartbeat`)
|
|
||||||
|
|
||||||
if (! previousBeat) {
|
if (! previousBeat) {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
|
@ -56,13 +57,15 @@ class Monitor extends BeanModel {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFirstBeat = !previousBeat;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat")
|
let bean = R.dispense("heartbeat")
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = 0;
|
bean.status = DOWN;
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
if (previousBeat) {
|
if (! isFirstBeat) {
|
||||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
|
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
|
||||||
} else {
|
} else {
|
||||||
bean.duration = 0;
|
bean.duration = 0;
|
||||||
|
@ -78,7 +81,7 @@ class Monitor extends BeanModel {
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = 1;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
@ -90,7 +93,7 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
if (data.includes(this.keyword)) {
|
||||||
bean.msg += ", keyword is found"
|
bean.msg += ", keyword is found"
|
||||||
bean.status = 1;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found")
|
throw new Error(bean.msg + ", but keyword is not found")
|
||||||
}
|
}
|
||||||
|
@ -101,32 +104,52 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
bean.ping = await tcping(this.hostname, this.port);
|
bean.ping = await tcping(this.hostname, this.port);
|
||||||
bean.msg = ""
|
bean.msg = ""
|
||||||
bean.status = 1;
|
bean.status = UP;
|
||||||
|
|
||||||
} else if (this.type === "ping") {
|
} else if (this.type === "ping") {
|
||||||
bean.ping = await ping(this.hostname);
|
bean.ping = await ping(this.hostname);
|
||||||
bean.msg = ""
|
bean.msg = ""
|
||||||
bean.status = 1;
|
bean.status = UP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retries = 0;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||||
|
retries++;
|
||||||
|
bean.status = PENDING;
|
||||||
|
}
|
||||||
bean.msg = error.message;
|
bean.msg = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as important if status changed
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
if (! previousBeat || previousBeat.status !== bean.status) {
|
// UP -> PENDING = not important
|
||||||
|
// * UP -> DOWN = important
|
||||||
|
// UP -> UP = not important
|
||||||
|
// PENDING -> PENDING = not important
|
||||||
|
// * PENDING -> DOWN = important
|
||||||
|
// PENDING -> UP = not important
|
||||||
|
// DOWN -> PENDING = this case not exists
|
||||||
|
// DOWN -> DOWN = not important
|
||||||
|
// * DOWN -> UP = important
|
||||||
|
let isImportant = isFirstBeat ||
|
||||||
|
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||||
|
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||||
|
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||||
|
|
||||||
|
// Mark as important if status changed, ignore pending pings,
|
||||||
|
// Don't notify if disrupted changes to up
|
||||||
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
|
||||||
// Do not send if first beat is UP
|
// Send only if the first beat is DOWN
|
||||||
if (previousBeat || bean.status !== 1) {
|
if (!isFirstBeat || bean.status === DOWN) {
|
||||||
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [
|
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [
|
||||||
this.id
|
this.id
|
||||||
])
|
])
|
||||||
|
|
||||||
let promiseList = [];
|
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (bean.status === 1) {
|
if (bean.status === UP) {
|
||||||
text = "✅ Up"
|
text = "✅ Up"
|
||||||
} else {
|
} else {
|
||||||
text = "🔴 Down"
|
text = "🔴 Down"
|
||||||
|
@ -135,16 +158,26 @@ class Monitor extends BeanModel {
|
||||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||||
|
|
||||||
for(let notification of notificationList) {
|
for(let notification of notificationList) {
|
||||||
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
|
try {
|
||||||
|
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Cannot send notification to " + notification.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promiseList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bean.status === UP) {
|
||||||
|
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
||||||
|
} else if (bean.status === PENDING) {
|
||||||
|
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Type: ${this.type}`)
|
||||||
|
} else {
|
||||||
|
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||||
|
}
|
||||||
|
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
|
|
||||||
await R.store(bean)
|
await R.store(bean)
|
||||||
|
@ -233,7 +266,7 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
total += value;
|
total += value;
|
||||||
if (row.status === 0) {
|
if (row.status === 0 || row.status === 2) {
|
||||||
downtime += value;
|
downtime += value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { R } = require("redbean-node");
|
const {R} = require("redbean-node");
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param notification
|
||||||
|
* @param msg
|
||||||
|
* @param monitorJSON
|
||||||
|
* @param heartbeatJSON
|
||||||
|
* @returns {Promise<string>} Successful msg
|
||||||
|
* Throw Error with fail msg
|
||||||
|
*/
|
||||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully. ";
|
||||||
|
|
||||||
if (notification.type === "telegram") {
|
if (notification.type === "telegram") {
|
||||||
try {
|
try {
|
||||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||||
|
@ -13,15 +26,16 @@ class Notification {
|
||||||
text: msg,
|
text: msg,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return true;
|
return okMsg;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||||
return false;
|
throw new Error(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (notification.type === "gotify") {
|
} else if (notification.type === "gotify") {
|
||||||
try {
|
try {
|
||||||
if (notification.gotifyserverurl.endsWith("/")) {
|
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||||
}
|
}
|
||||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||||
|
@ -29,135 +43,15 @@ class Notification {
|
||||||
"priority": notification.gotifyPriority || 8,
|
"priority": notification.gotifyPriority || 8,
|
||||||
"title": "Uptime-Kuma"
|
"title": "Uptime-Kuma"
|
||||||
})
|
})
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (notification.type === "pushover") {
|
return okMsg;
|
||||||
try {
|
|
||||||
await axios.post("https://api.pushover.net/1/messages.json", {
|
|
||||||
"message": msg,
|
|
||||||
"token": notification.pushoverAppToken,
|
|
||||||
"user": notification.pushoverUserKey,
|
|
||||||
"title": "Uptime-Kuma"
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (notification.type === "pushy") {
|
|
||||||
try {
|
|
||||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
|
||||||
"to": notification.pushyToken,
|
|
||||||
"data": {
|
|
||||||
"message": "Uptime-Kuma"
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"body": msg,
|
|
||||||
"badge": 1,
|
|
||||||
"sound": "ping.aiff"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
throwGeneralAxiosError(error)
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (notification.type === "slack") {
|
|
||||||
try {
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
let data = {
|
|
||||||
"blocks": [{
|
|
||||||
"type": "header",
|
|
||||||
"text": {
|
|
||||||
"type": "plain_text",
|
|
||||||
"text": "Uptime Kuma - Slack Testing"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "section",
|
|
||||||
"fields": [{
|
|
||||||
"type": "mrkdwn",
|
|
||||||
"text": "*Message*\nSlack Testing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "mrkdwn",
|
|
||||||
"text": "*Time (UTC)*\nSlack Testing"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "actions",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "button",
|
|
||||||
"text": {
|
|
||||||
"type": "plain_text",
|
|
||||||
"text": "Visit Uptime Kuma",
|
|
||||||
},
|
|
||||||
"value": "Uptime-Kuma",
|
|
||||||
"url": notification.slackbutton
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
let res = await axios.post(notification.slackwebhookURL, data)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = heartbeatJSON["time"];
|
|
||||||
let data = {
|
|
||||||
"blocks": [{
|
|
||||||
"type": "header",
|
|
||||||
"text": {
|
|
||||||
"type": "plain_text",
|
|
||||||
"text": "Uptime Kuma Alert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "section",
|
|
||||||
"fields": [{
|
|
||||||
"type": "mrkdwn",
|
|
||||||
"text": '*Message*\n' + msg
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "mrkdwn",
|
|
||||||
"text": "*Time (UTC)*\n" + time
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "actions",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "button",
|
|
||||||
"text": {
|
|
||||||
"type": "plain_text",
|
|
||||||
"text": "Visit Uptime Kuma",
|
|
||||||
},
|
|
||||||
"value": "Uptime-Kuma",
|
|
||||||
"url": notification.slackbutton
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
let res = await axios.post(notification.slackwebhookURL, data)
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (notification.type === "webhook") {
|
} else if (notification.type === "webhook") {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
heartbeat: heartbeatJSON,
|
heartbeat: heartbeatJSON,
|
||||||
monitor: monitorJSON,
|
monitor: monitorJSON,
|
||||||
|
@ -178,11 +72,11 @@ class Notification {
|
||||||
finalData = data;
|
finalData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await axios.post(notification.webhookURL, finalData, config)
|
await axios.post(notification.webhookURL, finalData, config)
|
||||||
return true;
|
return okMsg;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
throwGeneralAxiosError(error)
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (notification.type === "smtp") {
|
} else if (notification.type === "smtp") {
|
||||||
|
@ -190,61 +84,146 @@ class Notification {
|
||||||
|
|
||||||
} else if (notification.type === "discord") {
|
} else if (notification.type === "discord") {
|
||||||
try {
|
try {
|
||||||
// If heartbeatJSON is null, assume we're testing.
|
// If heartbeatJSON is null, assume we're testing.
|
||||||
if (heartbeatJSON == null) {
|
if(heartbeatJSON == null) {
|
||||||
let data = {
|
|
||||||
username: 'Uptime-Kuma',
|
|
||||||
content: msg
|
|
||||||
}
|
|
||||||
let res = await axios.post(notification.discordWebhookUrl, data)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
|
||||||
if (heartbeatJSON['status'] == 0) {
|
|
||||||
var alertColor = "16711680";
|
|
||||||
} else if (heartbeatJSON['status'] == 1) {
|
|
||||||
var alertColor = "65280";
|
|
||||||
}
|
|
||||||
let data = {
|
let data = {
|
||||||
username: 'Uptime-Kuma',
|
username: 'Uptime-Kuma',
|
||||||
embeds: [{
|
content: msg
|
||||||
title: "Uptime-Kuma Alert",
|
|
||||||
color: alertColor,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: "Time (UTC)",
|
|
||||||
value: heartbeatJSON["time"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Message",
|
|
||||||
value: msg
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
let res = await axios.post(notification.discordWebhookUrl, data)
|
await axios.post(notification.discordWebhookUrl, data)
|
||||||
return true;
|
return okMsg;
|
||||||
} catch (error) {
|
}
|
||||||
console.log(error)
|
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||||
return false;
|
if(heartbeatJSON['status'] == 0) {
|
||||||
|
var alertColor = "16711680";
|
||||||
|
} else if(heartbeatJSON['status'] == 1) {
|
||||||
|
var alertColor = "65280";
|
||||||
|
}
|
||||||
|
let data = {
|
||||||
|
username: 'Uptime-Kuma',
|
||||||
|
embeds: [{
|
||||||
|
title: "Uptime-Kuma Alert",
|
||||||
|
color: alertColor,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Time (UTC)",
|
||||||
|
value: heartbeatJSON["time"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Message",
|
||||||
|
value: msg
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
await axios.post(notification.discordWebhookUrl, data)
|
||||||
|
return okMsg;
|
||||||
|
} catch(error) {
|
||||||
|
throwGeneralAxiosError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (notification.type === "signal") {
|
} else if (notification.type === "signal") {
|
||||||
try {
|
try {
|
||||||
let data = {
|
let data = {
|
||||||
"message": msg,
|
"message": msg,
|
||||||
"number": notification.signalNumber,
|
"number": notification.signalNumber,
|
||||||
"recipients": notification.signalRecipients.replace(/\s/g, '').split(",")
|
"recipients": notification.signalRecipients.replace(/\s/g, '').split(",")
|
||||||
};
|
};
|
||||||
let config = {};
|
let config = {};
|
||||||
|
|
||||||
let res = await axios.post(notification.signalURL, data, config)
|
await axios.post(notification.signalURL, data, config)
|
||||||
return true;
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
throwGeneralAxiosError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (notification.type === "slack") {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo}
|
||||||
|
await axios.post(notification.slackwebhookURL, data)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = heartbeatJSON["time"];
|
||||||
|
let data = {
|
||||||
|
"text": "Uptime Kuma Alert",
|
||||||
|
"channel":notification.slackchannel,
|
||||||
|
"username": notification.slackusername,
|
||||||
|
"icon_emoji": notification.slackiconemo,
|
||||||
|
"blocks": [{
|
||||||
|
"type": "header",
|
||||||
|
"text": {
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": "Uptime Kuma Alert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"fields": [{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": '*Message*\n'+msg
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "*Time (UTC)*\n"+time
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "actions",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"text": {
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": "Visit Uptime Kuma",
|
||||||
|
},
|
||||||
|
"value": "Uptime-Kuma",
|
||||||
|
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await axios.post(notification.slackwebhookURL, data)
|
||||||
|
return okMsg;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
throwGeneralAxiosError(error)
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (notification.type === "pushover") {
|
||||||
|
var pushoverlink = 'https://api.pushover.net/1/messages.json'
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||||
|
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds,
|
||||||
|
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1}
|
||||||
|
await axios.post(pushoverlink, data)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:"+msg+ '\n<b>Time (UTC)</b>:' +heartbeatJSON["time"],
|
||||||
|
"user":notification.pushoveruserkey,
|
||||||
|
"token": notification.pushoverapptoken,
|
||||||
|
"sound": notification.pushoversounds,
|
||||||
|
"priority": notification.pushoverpriority,
|
||||||
|
"title": notification.pushovertitle,
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "3600",
|
||||||
|
"html": 1
|
||||||
|
}
|
||||||
|
await axios.post(pushoverlink, data)
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
throwGeneralAxiosError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (notification.type === "apprise") {
|
||||||
|
|
||||||
|
return Notification.apprise(notification, msg)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Notification type is not supported")
|
throw new Error("Notification type is not supported")
|
||||||
}
|
}
|
||||||
|
@ -259,7 +238,7 @@ class Notification {
|
||||||
userID,
|
userID,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!bean) {
|
if (! bean) {
|
||||||
throw new Error("notification not found")
|
throw new Error("notification not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +258,7 @@ class Notification {
|
||||||
userID,
|
userID,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!bean) {
|
if (! bean) {
|
||||||
throw new Error("notification not found")
|
throw new Error("notification not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,27 +278,54 @@ class Notification {
|
||||||
});
|
});
|
||||||
|
|
||||||
// send mail with defined transport object
|
// send mail with defined transport object
|
||||||
let info = await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
||||||
to: notification.smtpTo,
|
to: notification.smtpTo,
|
||||||
subject: msg,
|
subject: msg,
|
||||||
text: msg,
|
text: msg,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return "Sent Successfully.";
|
||||||
}
|
}
|
||||||
|
|
||||||
static async discord(notification, msg) {
|
static async apprise(notification, msg) {
|
||||||
const client = new Discord.Client();
|
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||||
await client.login(notification.discordToken)
|
|
||||||
|
|
||||||
const channel = await client.channels.fetch(notification.discordChannelID);
|
|
||||||
await channel.send(msg);
|
|
||||||
|
|
||||||
client.destroy()
|
let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found';
|
||||||
|
|
||||||
return true;
|
if (output) {
|
||||||
|
|
||||||
|
if (! output.includes("ERROR")) {
|
||||||
|
return "Sent Successfully";
|
||||||
|
} else {
|
||||||
|
throw new Error(output)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static checkApprise() {
|
||||||
|
let commandExistsSync = require('command-exists').sync;
|
||||||
|
let exists = commandExistsSync('apprise');
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwGeneralAxiosError(error) {
|
||||||
|
let msg = "Error: " + error + " ";
|
||||||
|
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
if (typeof error.response.data === "string") {
|
||||||
|
msg += error.response.data;
|
||||||
|
} else {
|
||||||
|
msg += JSON.stringify(error.response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
151
server/server.js
151
server/server.js
|
@ -1,46 +1,75 @@
|
||||||
|
console.log("Welcome to Uptime Kuma ")
|
||||||
|
console.log("Importing libraries")
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const server = http.createServer(app);
|
|
||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
const io = new Server(server);
|
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { R } = require("redbean-node");
|
const {R} = require("redbean-node");
|
||||||
const passwordHash = require('./password-hash');
|
const passwordHash = require('./password-hash');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { getSettings } = require("./util-server");
|
const {getSettings} = require("./util-server");
|
||||||
const { Notification } = require("./notification")
|
const {Notification} = require("./notification")
|
||||||
|
const gracefulShutdown = require('http-graceful-shutdown');
|
||||||
|
const Database = require("./database");
|
||||||
|
const {sleep} = require("./util");
|
||||||
const args = require('args-parser')(process.argv);
|
const args = require('args-parser')(process.argv);
|
||||||
|
|
||||||
const version = require('../package.json').version;
|
const version = require('../package.json').version;
|
||||||
const hostname = args.host || "0.0.0.0"
|
const hostname = args.host || "0.0.0.0"
|
||||||
const port = args.port || 3001
|
const port = args.port || 3001
|
||||||
|
|
||||||
|
console.info("Version: " + version)
|
||||||
|
|
||||||
|
console.log("Creating express and socket.io instance")
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new Server(server);
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total WebSocket client connected to server currently, no actual use
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
let totalClient = 0;
|
let totalClient = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use for decode the auth object
|
||||||
|
* @type {null}
|
||||||
|
*/
|
||||||
let jwtSecret = null;
|
let jwtSecret = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main monitor list
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
let monitorList = {};
|
let monitorList = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Setup Page
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
let needSetup = false;
|
let needSetup = false;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await initDatabase();
|
await initDatabase();
|
||||||
|
|
||||||
|
console.log("Adding route")
|
||||||
app.use('/', express.static("dist"));
|
app.use('/', express.static("dist"));
|
||||||
|
|
||||||
app.get('*', function (request, response, next) {
|
app.get('*', function(request, response, next) {
|
||||||
response.sendFile(process.cwd() + '/dist/index.html');
|
response.sendFile(process.cwd() + '/dist/index.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Adding socket handler")
|
||||||
io.on('connection', async (socket) => {
|
io.on('connection', async (socket) => {
|
||||||
|
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version,
|
version,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('a user connected');
|
|
||||||
totalClient++;
|
totalClient++;
|
||||||
|
|
||||||
if (needSetup) {
|
if (needSetup) {
|
||||||
|
@ -49,7 +78,6 @@ let needSetup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log('user disconnected');
|
|
||||||
totalClient--;
|
totalClient--;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -155,10 +183,6 @@ let needSetup = false;
|
||||||
msg: e.message
|
msg: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auth Only API
|
// Auth Only API
|
||||||
|
@ -198,7 +222,7 @@ let needSetup = false;
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
|
||||||
let bean = await R.findOne("monitor", " id = ? ", [monitor.id])
|
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ])
|
||||||
|
|
||||||
if (bean.user_id !== socket.userID) {
|
if (bean.user_id !== socket.userID) {
|
||||||
throw new Error("Permission denied.")
|
throw new Error("Permission denied.")
|
||||||
|
@ -209,6 +233,7 @@ let needSetup = false;
|
||||||
bean.url = monitor.url
|
bean.url = monitor.url
|
||||||
bean.interval = monitor.interval
|
bean.interval = monitor.interval
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
|
bean.maxretries = monitor.maxretries;
|
||||||
bean.port = monitor.port;
|
bean.port = monitor.port;
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
|
|
||||||
|
@ -229,7 +254,7 @@ let needSetup = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.error(e)
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message
|
||||||
|
@ -336,7 +361,7 @@ let needSetup = false;
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
|
||||||
if (!password.currentPassword) {
|
if (! password.currentPassword) {
|
||||||
throw new Error("Invalid new password")
|
throw new Error("Invalid new password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,25 +455,36 @@ let needSetup = false;
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
|
||||||
await Notification.send(notification, notification.name + " Testing")
|
let msg = await Notification.send(notification, notification.name + " Testing")
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Sent Successfully"
|
msg
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("checkApprise", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
callback(Notification.checkApprise());
|
||||||
|
} catch (e) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("Init")
|
||||||
server.listen(port, hostname, () => {
|
server.listen(port, hostname, () => {
|
||||||
console.log(`Listening on ${hostname}:${port}`);
|
console.log(`Listening on ${hostname}:${port}`);
|
||||||
|
|
||||||
startMonitors();
|
startMonitors();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -475,7 +511,7 @@ async function checkOwner(userID, monitorID) {
|
||||||
userID,
|
userID,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!row) {
|
if (! row) {
|
||||||
throw new Error("You do not own this monitor.");
|
throw new Error("You do not own this monitor.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -530,24 +566,27 @@ async function getMonitorJSONList(userID) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkLogin(socket) {
|
function checkLogin(socket) {
|
||||||
if (!socket.userID) {
|
if (! socket.userID) {
|
||||||
throw new Error("You are not logged in.");
|
throw new Error("You are not logged in.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDatabase() {
|
async function initDatabase() {
|
||||||
const path = './data/kuma.db';
|
if (! fs.existsSync(Database.path)) {
|
||||||
|
console.log("Copying Database")
|
||||||
if (!fs.existsSync(path)) {
|
fs.copyFileSync(Database.templatePath, Database.path);
|
||||||
console.log("Copy Database")
|
|
||||||
fs.copyFileSync("./db/kuma.db", path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Connect to Database")
|
console.log("Connecting to Database")
|
||||||
|
|
||||||
R.setup('sqlite', {
|
R.setup('sqlite', {
|
||||||
filename: path
|
filename: Database.path
|
||||||
});
|
});
|
||||||
|
console.log("Connected")
|
||||||
|
|
||||||
|
// Patch the database
|
||||||
|
await Database.patch()
|
||||||
|
|
||||||
|
// Auto map the model to a bean object
|
||||||
R.freeze(true)
|
R.freeze(true)
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
|
@ -555,17 +594,19 @@ async function initDatabase() {
|
||||||
"jwtSecret"
|
"jwtSecret"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!jwtSecretBean) {
|
if (! jwtSecretBean) {
|
||||||
console.log("JWT secret is not found, generate one.")
|
console.log("JWT secret is not found, generate one.")
|
||||||
jwtSecretBean = R.dispense("setting")
|
jwtSecretBean = R.dispense("setting")
|
||||||
jwtSecretBean.key = "jwtSecret"
|
jwtSecretBean.key = "jwtSecret"
|
||||||
|
|
||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
|
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
|
||||||
await R.store(jwtSecretBean)
|
await R.store(jwtSecretBean)
|
||||||
|
console.log("Stored JWT secret into database")
|
||||||
} else {
|
} else {
|
||||||
console.log("Load JWT secret from database.")
|
console.log("Load JWT secret from database.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
|
||||||
if ((await R.count("user")) === 0) {
|
if ((await R.count("user")) === 0) {
|
||||||
console.log("No user, need setup")
|
console.log("No user, need setup")
|
||||||
needSetup = true;
|
needSetup = true;
|
||||||
|
@ -642,7 +683,7 @@ async function sendHeartbeatList(socket, monitorID) {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
result.unshift(bean.toJSON())
|
result.unshift(bean.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("heartbeatList", monitorID, result)
|
socket.emit("heartbeatList", monitorID, result)
|
||||||
|
@ -660,3 +701,51 @@ async function sendImportantHeartbeatList(socket, monitorID) {
|
||||||
|
|
||||||
socket.emit("importantHeartbeatList", monitorID, list)
|
socket.emit("importantHeartbeatList", monitorID, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const startGracefulShutdown = async () => {
|
||||||
|
console.log('Shutdown requested');
|
||||||
|
|
||||||
|
|
||||||
|
await (new Promise((resolve) => {
|
||||||
|
server.close(async function () {
|
||||||
|
console.log('Stopped Express.');
|
||||||
|
process.exit(0)
|
||||||
|
setTimeout(async () =>{
|
||||||
|
await R.close();
|
||||||
|
console.log("Stopped DB")
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shutdownFunction(signal) {
|
||||||
|
console.log('Called signal: ' + signal);
|
||||||
|
|
||||||
|
console.log("Stopping all monitors")
|
||||||
|
for (let id in monitorList) {
|
||||||
|
let monitor = monitorList[id]
|
||||||
|
monitor.stop()
|
||||||
|
}
|
||||||
|
await sleep(2000);
|
||||||
|
await Database.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalFunction() {
|
||||||
|
console.log('Graceful Shutdown')
|
||||||
|
}
|
||||||
|
|
||||||
|
gracefulShutdown(server, {
|
||||||
|
signals: 'SIGINT SIGTERM',
|
||||||
|
timeout: 30000, // timeout: 30 secs
|
||||||
|
development: false, // not in dev mode
|
||||||
|
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||||
|
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
||||||
|
finally: finalFunction // finally function (sync) - e.g. for logging
|
||||||
|
});
|
||||||
|
|
|
@ -45,6 +45,18 @@ exports.setting = async function (key) {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.setSetting = async function (key, value) {
|
||||||
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
|
key
|
||||||
|
])
|
||||||
|
if (! bean) {
|
||||||
|
bean = R.dispense("setting")
|
||||||
|
bean.key = key;
|
||||||
|
}
|
||||||
|
bean.value = value;
|
||||||
|
await R.store(bean)
|
||||||
|
}
|
||||||
|
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
|
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
|
||||||
type
|
type
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
/*
|
// Common JS cannot be used in frontend sadly
|
||||||
* Common functions - can be used in frontend or backend
|
// sleep, ucfirst is duplicated in ../src/util-frontend.js
|
||||||
*/
|
|
||||||
|
|
||||||
|
exports.DOWN = 0;
|
||||||
|
exports.UP = 1;
|
||||||
|
exports.PENDING = 2;
|
||||||
|
|
||||||
|
exports.sleep = function (ms) {
|
||||||
|
|
||||||
export function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ucfirst(str) {
|
exports.ucfirst = function (str) {
|
||||||
if (! str) {
|
if (! str) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import {sleep} from "../../server/util";
|
import {sleep} from '../util-frontend'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin
|
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="hp-bar-big" :style="barStyle">
|
<div class="hp-bar-big" :style="barStyle">
|
||||||
<div
|
<div
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
|
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
@ -166,6 +166,10 @@ export default {
|
||||||
background-color: $danger;
|
background-color: $danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
&:not(.empty):hover {
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3 mt-3" >
|
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||||
<label>
|
<div class="form-check">
|
||||||
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
||||||
|
|
||||||
<label class="form-check-label" for="remember">
|
<label class="form-check-label" for="remember">
|
||||||
Remember me
|
Remember me
|
||||||
</label>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button>
|
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button>
|
||||||
|
|
||||||
|
|
|
@ -10,72 +10,72 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="type" class="form-label">Notification Type</label>
|
||||||
|
<select class="form-select" id="type" v-model="notification.type">
|
||||||
|
<option value="telegram">Telegram</option>
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
<option value="smtp">Email (SMTP)</option>
|
||||||
|
<option value="discord">Discord</option>
|
||||||
|
<option value="signal">Signal</option>
|
||||||
|
<option value="gotify">Gotify</option>
|
||||||
|
<option value="slack">Slack</option>
|
||||||
|
<option value="pushover">Pushover</option>
|
||||||
|
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Friendly Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="notification.type === 'telegram'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="type" class="form-label">Notification Type</label>
|
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||||
<select class="form-select" id="type" v-model="notification.type">
|
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken">
|
||||||
<option value="telegram">Telegram</option>
|
<div class="form-text">You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.</div>
|
||||||
<option value="webhook">Webhook</option>
|
|
||||||
<option value="smtp">Email (SMTP)</option>
|
|
||||||
<option value="discord">Discord</option>
|
|
||||||
<option value="signal">Signal</option>
|
|
||||||
<option value="gotify">Gotify</option>
|
|
||||||
<option value="slack">Slack</option>
|
|
||||||
<option value="pushover">Pushover</option>
|
|
||||||
<option value="pushy">Pushy</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Friendly Name</label>
|
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
Support Direct Chat / Group / Channel's Chat ID
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
|
||||||
|
<template v-if="notification.telegramBotToken">
|
||||||
|
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ telegramGetUpdatesURL }}
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<template v-if="notification.type === 'telegram'">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
|
||||||
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken">
|
|
||||||
<div class="form-text">You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
|
||||||
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-text">
|
|
||||||
Support Direct Chat / Group / Channel's Chat ID
|
|
||||||
|
|
||||||
<p style="margin-top: 8px;">
|
|
||||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p style="margin-top: 8px;">
|
|
||||||
|
|
||||||
<template v-if="notification.telegramBotToken">
|
|
||||||
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
{{ telegramGetUpdatesURL }}
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="notification.type === 'webhook'">
|
<template v-if="notification.type === 'webhook'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="webhook-url" class="form-label">Post URL</label>
|
<label for="webhook-url" class="form-label">Post URL</label>
|
||||||
<input type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL">
|
<input type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||||
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required>
|
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required>
|
||||||
<option value="json">application/json</option>
|
<option value="json">application/json</option>
|
||||||
<option value="form-data">multipart/form-data</option>
|
<option value="form-data">multipart/form-data</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
||||||
<label class="form-check-label" for="secure">
|
<label class="form-check-label" for="secure">
|
||||||
Secure
|
Secure
|
||||||
</label>
|
</label>
|
||||||
|
@ -141,7 +141,7 @@
|
||||||
<template v-if="notification.type === 'signal'">
|
<template v-if="notification.type === 'signal'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="signal-url" class="form-label">Post URL</label>
|
<label for="signal-url" class="form-label">Post URL</label>
|
||||||
<input type="url" pattern="https?://.+" class="form-control" id="signal-url" required v-model="notification.signalURL">
|
<input type="url" pattern="https?://.+" class="form-control" id="signal-url" required v-model="notification.signalURL">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -174,70 +174,129 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'gotify'">
|
<template v-if="notification.type === 'gotify'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||||
<input type="text" class="form-control" id="gotify-application-token" required v-model="notification.gotifyapplicationToken">
|
<input type="text" class="form-control" id="gotify-application-token" required v-model="notification.gotifyapplicationToken">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
<div class="mb-3">
|
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
<div class="input-group mb-3">
|
||||||
<div class="input-group mb-3">
|
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl">
|
||||||
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-priority" class="form-label">Priority</label>
|
<label for="gotify-priority" class="form-label">Priority</label>
|
||||||
<input type="number" class="form-control" id="gotify-priority" v-model="notification.gotifyPriority" required min="0" max="10" step="1">
|
<input type="number" class="form-control" id="gotify-priority" v-model="notification.gotifyPriority" required min="0" max="10" step="1">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'slack'">
|
<template v-if="notification.type === 'slack'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="slack-webhook-url" class="form-label">Slack Webhook URL</label>
|
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||||
<input type="text" class="form-control" id="slack-webhook-url" required v-model="notification.slackwebhookURL" autocomplete="false">
|
<input type="text" class="form-control" id="slack-webhook-url" required v-model="notification.slackwebhookURL">
|
||||||
<label for="uptimekuma-server-url" class="form-label">Uptime Kuma URL</label>
|
<label for="slack-username" class="form-label">Username</label>
|
||||||
<div class="input-group mb-3">
|
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername">
|
||||||
<input type="text" class="form-control" id="uptimekuma-server-url" required v-model="notification.slackbutton" autocomplete="false">
|
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||||
</div>
|
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo">
|
||||||
<p style="margin-top: 8px;">
|
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||||
More info on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel">
|
||||||
</p>
|
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||||
|
<input type="text" class="form-control" id="slack-button" v-model="notification.slackbutton">
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color:red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'pushover'">
|
<template v-if="notification.type === 'pushover'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushover-app-token" class="form-label">APP_TOKEN</label>
|
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
||||||
<input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverAppToken">
|
<input type="text" class="form-control" id="pushover-user" required v-model="notification.pushoveruserkey">
|
||||||
</div>
|
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
||||||
|
<input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken">
|
||||||
<div class="mb-3">
|
<label for="pushover-device" class="form-label">Device</label>
|
||||||
<label for="pushover-user-key" class="form-label">USER_KEY</label>
|
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice">
|
||||||
<div class="input-group mb-3">
|
<label for="pushover-device" class="form-label">Message Title</label>
|
||||||
<input type="text" class="form-control" id="pushover-user-key" required v-model="notification.pushoverUserKey">
|
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle">
|
||||||
|
<label for="pushover-priority" class="form-label">Priority</label>
|
||||||
|
<select class="form-select" id="pushover-priority" v-model="notification.pushoverpriority">
|
||||||
|
<option>-2</option>
|
||||||
|
<option>-1</option>
|
||||||
|
<option>0</option>
|
||||||
|
<option>1</option>
|
||||||
|
<option>2</option>
|
||||||
|
</select>
|
||||||
|
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||||
|
<select class="form-select" id="pushover-sound" v-model="notification.pushoversounds">
|
||||||
|
<option>pushover</option>
|
||||||
|
<option>bike</option>
|
||||||
|
<option>bugle</option>
|
||||||
|
<option>cashregister</option>
|
||||||
|
<option>classical</option>
|
||||||
|
<option>cosmic</option>
|
||||||
|
<option>falling</option>
|
||||||
|
<option>gamelan</option>
|
||||||
|
<option>incoming</option>
|
||||||
|
<option>intermission</option>
|
||||||
|
<option>mechanical</option>
|
||||||
|
<option>pianobar</option>
|
||||||
|
<option>siren</option>
|
||||||
|
<option>spacealarm</option>
|
||||||
|
<option>tugboat</option>
|
||||||
|
<option>alien</option>
|
||||||
|
<option>climb</option>
|
||||||
|
<option>persistent</option>
|
||||||
|
<option>echo</option>
|
||||||
|
<option>updown</option>
|
||||||
|
<option>vibrate</option>
|
||||||
|
<option>none</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color:red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you want to send notifications to different devices, fill out Device field.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-top: 8px;">
|
|
||||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
|
||||||
</p>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'pushy'">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
|
||||||
<input type="text" class="form-control" id="pushy-app-token" required v-model="notification.pushyAPIKey">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<template v-if="notification.type === 'apprise'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||||
<div class="input-group mb-3">
|
<input type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL">
|
||||||
<input type="text" class="form-control" id="pushy-user-key" required v-model="notification.pushyToken">
|
<div class="form-text">
|
||||||
|
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||||
|
<p>
|
||||||
|
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-top: 8px;">
|
<div class="mb-3">
|
||||||
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
<p>
|
||||||
</p>
|
Status:
|
||||||
|
<span class="text-primary" v-if="appriseInstalled">Apprise is installed</span>
|
||||||
|
<span class="text-danger" v-else>Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -257,7 +316,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Modal } from 'bootstrap'
|
import { Modal } from 'bootstrap'
|
||||||
import { ucfirst } from "../../server/util";
|
import { ucfirst } from '../util-frontend'
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
|
@ -278,17 +337,15 @@ export default {
|
||||||
type: null,
|
type: null,
|
||||||
gotifyPriority: 8
|
gotifyPriority: 8
|
||||||
},
|
},
|
||||||
|
appriseInstalled: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal)
|
||||||
|
|
||||||
// TODO: for edit
|
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||||
this.$root.getSocket().emit("getSettings", "notification", (data) => {
|
this.appriseInstalled = installed;
|
||||||
// this.notification = data
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ export default {
|
||||||
return "danger"
|
return "danger"
|
||||||
} else if (this.status === 1) {
|
} else if (this.status === 1) {
|
||||||
return "primary"
|
return "primary"
|
||||||
|
} else if (this.status === 2) {
|
||||||
|
return "warning"
|
||||||
} else {
|
} else {
|
||||||
return "secondary"
|
return "secondary"
|
||||||
}
|
}
|
||||||
|
@ -24,6 +26,8 @@ export default {
|
||||||
return "Down"
|
return "Down"
|
||||||
} else if (this.status === 1) {
|
} else if (this.status === 1) {
|
||||||
return "Up"
|
return "Up"
|
||||||
|
} else if (this.status === 2) {
|
||||||
|
return "Pending"
|
||||||
} else {
|
} else {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
@ -34,6 +38,6 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
span {
|
span {
|
||||||
width: 45px;
|
width: 54px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -30,6 +30,8 @@ export default {
|
||||||
return "danger"
|
return "danger"
|
||||||
} else if (this.lastHeartBeat.status === 1) {
|
} else if (this.lastHeartBeat.status === 1) {
|
||||||
return "primary"
|
return "primary"
|
||||||
|
} else if (this.lastHeartBeat.status === 2) {
|
||||||
|
return "warning"
|
||||||
} else {
|
} else {
|
||||||
return "secondary"
|
return "secondary"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="lost-connection" v-if="!$root.socket.connected && !$root.socket.firstConnect">
|
|
||||||
<div class="container-fluid">Lost connection to the socket server. Reconnecting...</div>
|
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
|
||||||
|
<div class="container-fluid">
|
||||||
|
Lost connection to the socket server. Reconnecting...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Desktop header -->
|
<!-- Desktop header -->
|
||||||
<header
|
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile">
|
||||||
class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom"
|
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||||
v-if="!$root.isMobile"
|
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo"></object>
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
to="/dashboard"
|
|
||||||
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-decoration-none"
|
|
||||||
>
|
|
||||||
<img class="bi me-2 ms-4" width="40" height="40" src="/icon.svg" />
|
|
||||||
<span class="fs-4 title">Uptime Kuma</span>
|
<span class="fs-4 title">Uptime Kuma</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills" >
|
||||||
<li class="nav-item">
|
<li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li>
|
||||||
<router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link>
|
<li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li>
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<router-link to="/settings" class="nav-link">🔧 Settings</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mobile header -->
|
<!-- Mobile header -->
|
||||||
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else>
|
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else>
|
||||||
<router-link to="/dashboard" class="d-flex align-items-center text-decoration-none">
|
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||||
<img style="height:2rem;" src="/icon.svg" />
|
<object class="bi" width="40" height="40" data="/icon.svg"></object>
|
||||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
|
@ -37,39 +30,24 @@
|
||||||
<main>
|
<main>
|
||||||
<!-- Add :key to disable vue router re-use the same component -->
|
<!-- Add :key to disable vue router re-use the same component -->
|
||||||
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
|
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
|
||||||
<Login v-if="!$root.loggedIn && $root.allowLoginDialog" />
|
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
Uptime Kuma -
|
Uptime Kuma -
|
||||||
Version: {{ $root.info.version }} -
|
Version: {{ $root.info.version }} -
|
||||||
<a
|
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
|
||||||
href="https://github.com/louislam/uptime-kuma/releases"
|
|
||||||
target="_blank"
|
|
||||||
>Check Update On GitHub</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Mobile Only -->
|
<!-- Mobile Only -->
|
||||||
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
|
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
|
||||||
<nav class="bottom-nav" v-if="$root.isMobile">
|
<nav class="bottom-nav" v-if="$root.isMobile">
|
||||||
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
|
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link>
|
||||||
<div>📊</div>Dashboard
|
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a>
|
||||||
</router-link>
|
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div>➕</div>Add</router-link>
|
||||||
<a
|
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link>
|
||||||
href="#"
|
|
||||||
:class="{ 'router-link-exact-active': $root.showListMobile }"
|
|
||||||
@click="$root.showListMobile = !$root.showListMobile"
|
|
||||||
>
|
|
||||||
<div>📃</div>List
|
|
||||||
</a>
|
|
||||||
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
|
|
||||||
<div>➕</div>Add
|
|
||||||
</router-link>
|
|
||||||
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
|
|
||||||
<div>🔧</div>Settings
|
|
||||||
</router-link>
|
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -92,7 +70,7 @@ export default {
|
||||||
this.init();
|
this.init();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route (to, from) {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -117,9 +95,8 @@ export default {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: var(--background-navbar);
|
background-color: #fff;
|
||||||
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05),
|
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05);
|
||||||
0 5px 14px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0 35px;
|
padding: 0 35px;
|
||||||
|
@ -131,7 +108,7 @@ export default {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 8px 10px 0;
|
padding: 8px 10px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--main-font-color);
|
color: #c1c1c1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -160,9 +137,16 @@ export default {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
font-size: 0.85rem;
|
color: #AAA;
|
||||||
|
font-size: 13px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { io } from "socket.io-client";
|
import {io} from "socket.io-client";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
@ -20,11 +20,11 @@ export default {
|
||||||
userTimezone: localStorage.timezone || "auto",
|
userTimezone: localStorage.timezone || "auto",
|
||||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
monitorList: {},
|
monitorList: { },
|
||||||
heartbeatList: {},
|
heartbeatList: { },
|
||||||
importantHeartbeatList: {},
|
importantHeartbeatList: { },
|
||||||
avgPingList: {},
|
avgPingList: { },
|
||||||
uptimeList: {},
|
uptimeList: { },
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
showListMobile: false,
|
showListMobile: false,
|
||||||
|
@ -34,12 +34,22 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
|
|
||||||
const wsHost = ":50013"
|
let wsHost;
|
||||||
|
const env = process.env.NODE_ENV || "production";
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
wsHost = ":3001"
|
||||||
|
} else {
|
||||||
|
wsHost = ""
|
||||||
|
}
|
||||||
|
|
||||||
socket = io(wsHost, {
|
socket = io(wsHost, {
|
||||||
transports: ['websocket']
|
transports: ['websocket']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err) => {
|
||||||
|
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('info', (info) => {
|
socket.on('info', (info) => {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
});
|
});
|
||||||
|
@ -57,7 +67,7 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('heartbeat', (data) => {
|
socket.on('heartbeat', (data) => {
|
||||||
if (!(data.monitorID in this.heartbeatList)) {
|
if (! (data.monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[data.monitorID] = [];
|
this.heartbeatList[data.monitorID] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +90,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!(data.monitorID in this.importantHeartbeatList)) {
|
if (! (data.monitorID in this.importantHeartbeatList)) {
|
||||||
this.importantHeartbeatList[data.monitorID] = [];
|
this.importantHeartbeatList[data.monitorID] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +99,7 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('heartbeatList', (monitorID, data) => {
|
socket.on('heartbeatList', (monitorID, data) => {
|
||||||
if (!(monitorID in this.heartbeatList)) {
|
if (! (monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[monitorID] = data;
|
this.heartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
|
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
|
||||||
|
@ -105,7 +115,7 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('importantHeartbeatList', (monitorID, data) => {
|
socket.on('importantHeartbeatList', (monitorID, data) => {
|
||||||
if (!(monitorID in this.importantHeartbeatList)) {
|
if (! (monitorID in this.importantHeartbeatList)) {
|
||||||
this.importantHeartbeatList[monitorID] = data;
|
this.importantHeartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
|
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
|
||||||
|
@ -187,7 +197,7 @@ export default {
|
||||||
socket.emit("loginByToken", token, (res) => {
|
socket.emit("loginByToken", token, (res) => {
|
||||||
this.allowLoginDialog = true;
|
this.allowLoginDialog = true;
|
||||||
|
|
||||||
if (!res.ok) {
|
if (! res.ok) {
|
||||||
this.logout()
|
this.logout()
|
||||||
} else {
|
} else {
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
|
@ -257,7 +267,7 @@ export default {
|
||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
let lastHeartBeat = this.lastHeartbeatList[monitorID]
|
let lastHeartBeat = this.lastHeartbeatList[monitorID]
|
||||||
|
|
||||||
if (!lastHeartBeat) {
|
if (! lastHeartBeat) {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
} else if (lastHeartBeat.status === 1) {
|
} else if (lastHeartBeat.status === 1) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
|
@ -269,6 +279,11 @@ export default {
|
||||||
text: "Down",
|
text: "Down",
|
||||||
color: "danger"
|
color: "danger"
|
||||||
};
|
};
|
||||||
|
} else if (lastHeartBeat.status === 2) {
|
||||||
|
result[monitorID] = {
|
||||||
|
text: "Pending",
|
||||||
|
color: "warning"
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link v-bind:key="item.id" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in sortedMonitorList" @click="$root.cancelActiveList">
|
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-md-8 small-padding">
|
<div class="col-6 col-md-8 small-padding">
|
||||||
|
@ -115,7 +115,7 @@ export default {
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 15px 15px 12px 15px;
|
padding: 15px 15px 12px 15px;
|
||||||
border-radius: .25rem;
|
border-radius: 10px;
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -129,11 +129,11 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-sidebar-active);
|
background-color: $highlight-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--background-sidebar-active);
|
background-color: #cdf8f4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="beat in importantHeartBeatList">
|
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||||
<td>{{ beat.name }}</td>
|
<td>{{ beat.name }}</td>
|
||||||
<td><Status :status="beat.status" /></td>
|
<td><Status :status="beat.status" /></td>
|
||||||
<td><Datetime :value="beat.time" /></td>
|
<td><Datetime :value="beat.time" /></td>
|
||||||
|
@ -59,6 +59,13 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center kuma_pagination">
|
||||||
|
<pagination
|
||||||
|
v-model="page"
|
||||||
|
:records=importantHeartBeatList.length
|
||||||
|
:per-page="perPage" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -68,8 +75,21 @@
|
||||||
<script>
|
<script>
|
||||||
import Status from "../components/Status.vue";
|
import Status from "../components/Status.vue";
|
||||||
import Datetime from "../components/Datetime.vue";
|
import Datetime from "../components/Datetime.vue";
|
||||||
|
import Pagination from "v-pagination-3";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {Datetime, Status},
|
components: {
|
||||||
|
Datetime,
|
||||||
|
Status,
|
||||||
|
Pagination,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
page: 1,
|
||||||
|
perPage: 25,
|
||||||
|
heartBeatList: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
stats() {
|
stats() {
|
||||||
let result = {
|
let result = {
|
||||||
|
@ -90,6 +110,8 @@ export default {
|
||||||
result.up++;
|
result.up++;
|
||||||
} else if (beat.status === 0) {
|
} else if (beat.status === 0) {
|
||||||
result.down++;
|
result.down++;
|
||||||
|
} else if (beat.status === 2) {
|
||||||
|
result.up++;
|
||||||
} else {
|
} else {
|
||||||
result.unknown++;
|
result.unknown++;
|
||||||
}
|
}
|
||||||
|
@ -127,8 +149,16 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.heartBeatList = result;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
displayedRecords() {
|
||||||
|
const startIndex = this.perPage * (this.page - 1);
|
||||||
|
const endIndex = startIndex + this.perPage;
|
||||||
|
return this.heartBeatList.slice(startIndex, endIndex);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<div class="functions">
|
<div class="functions">
|
||||||
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
|
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
|
||||||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
||||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
|
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
|
||||||
<button class="btn btn-danger" @click="deleteDialog">Delete</button>
|
<button class="btn btn-danger" @click="deleteDialog">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="beat in importantHeartBeatList">
|
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||||
<td><Status :status="beat.status" /></td>
|
<td><Status :status="beat.status" /></td>
|
||||||
<td><Datetime :value="beat.time" /></td>
|
<td><Datetime :value="beat.time" /></td>
|
||||||
<td>{{ beat.msg }}</td>
|
<td>{{ beat.msg }}</td>
|
||||||
|
@ -75,6 +75,13 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center kuma_pagination">
|
||||||
|
<pagination
|
||||||
|
v-model="page"
|
||||||
|
:records=importantHeartBeatList.length
|
||||||
|
:per-page="perPage" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Confirm ref="confirmPause" @yes="pauseMonitor">
|
<Confirm ref="confirmPause" @yes="pauseMonitor">
|
||||||
|
@ -95,6 +102,7 @@ import Status from "../components/Status.vue";
|
||||||
import Datetime from "../components/Datetime.vue";
|
import Datetime from "../components/Datetime.vue";
|
||||||
import CountUp from "../components/CountUp.vue";
|
import CountUp from "../components/CountUp.vue";
|
||||||
import Uptime from "../components/Uptime.vue";
|
import Uptime from "../components/Uptime.vue";
|
||||||
|
import Pagination from "v-pagination-3";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -104,13 +112,16 @@ export default {
|
||||||
HeartbeatBar,
|
HeartbeatBar,
|
||||||
Confirm,
|
Confirm,
|
||||||
Status,
|
Status,
|
||||||
|
Pagination,
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
page: 1,
|
||||||
|
perPage: 25,
|
||||||
|
heartBeatList: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -154,6 +165,7 @@ export default {
|
||||||
|
|
||||||
importantHeartBeatList() {
|
importantHeartBeatList() {
|
||||||
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
||||||
|
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
|
||||||
return this.$root.importantHeartbeatList[this.monitor.id]
|
return this.$root.importantHeartbeatList[this.monitor.id]
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
@ -166,8 +178,13 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return { }
|
return { }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
displayedRecords() {
|
||||||
|
const startIndex = this.perPage * (this.page - 1);
|
||||||
|
const endIndex = startIndex + this.perPage;
|
||||||
|
return this.heartBeatList.slice(startIndex, endIndex);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
testNotification() {
|
testNotification() {
|
||||||
|
|
|
@ -45,7 +45,13 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||||
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="1" step="1">
|
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="maxRetries" class="form-label">Retries</label>
|
||||||
|
<input type="number" class="form-control" id="maxRetries" v-model="monitor.maxretries" required min="0" step="1">
|
||||||
|
<div class="form-text">Maximum retries before the service is marked as down and a notification is sent</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -61,7 +67,7 @@
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
||||||
|
|
||||||
<div v-bind:key="notification.id" class="form-check form-switch mb-3" v-for="notification in $root.notificationList">
|
<div class="form-check form-switch mb-3" :key="notification.id" v-for="notification in $root.notificationList">
|
||||||
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
|
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
|
||||||
|
|
||||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||||
|
@ -118,7 +124,8 @@ export default {
|
||||||
type: "http",
|
type: "http",
|
||||||
name: "",
|
name: "",
|
||||||
url: "https://",
|
url: "https://",
|
||||||
interval: 20,
|
interval: 60,
|
||||||
|
maxretries: 0,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
}
|
}
|
||||||
} else if (this.isEdit) {
|
} else if (this.isEdit) {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<label for="timezone" class="form-label">Timezone</label>
|
<label for="timezone" class="form-label">Timezone</label>
|
||||||
<select class="form-select" id="timezone" v-model="$root.userTimezone">
|
<select class="form-select" id="timezone" v-model="$root.userTimezone">
|
||||||
<option value="auto">Auto: {{ guessTimezone }}</option>
|
<option value="auto">Auto: {{ guessTimezone }}</option>
|
||||||
<option v-for="timezone in timezoneList" :value="timezone.value">{{ timezone.name }}</option>
|
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||||
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword">
|
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword">
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
The repeat password is not match.
|
The repeat password does not match.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -56,10 +56,10 @@
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
||||||
<p v-else>Please assign the notification to monitor(s) to get it works.</p>
|
<p v-else>Please assign a notification to monitor(s) to get it to work.</p>
|
||||||
|
|
||||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
<li class="list-group-item" v-for="notification in $root.notificationList">
|
<li class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index">
|
||||||
{{ notification.name }}<br />
|
{{ notification.name }}<br />
|
||||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -77,8 +77,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
export function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ucfirst(str) {
|
||||||
|
if (! str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLetter = str.substr(0, 1);
|
||||||
|
return firstLetter.toUpperCase() + str.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getTimezoneOffset(timeZone) {
|
function getTimezoneOffset(timeZone) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -16,6 +29,7 @@ function getTimezoneOffset(timeZone) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
||||||
|
// TODO: Move to separate file
|
||||||
const aryIannaTimeZones = [
|
const aryIannaTimeZones = [
|
||||||
'Europe/Andorra',
|
'Europe/Andorra',
|
||||||
'Asia/Dubai',
|
'Asia/Dubai',
|
||||||
|
@ -381,7 +395,7 @@ export function timezoneList() {
|
||||||
time: getTimezoneOffset(timezone),
|
time: getTimezoneOffset(timezone),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e.message);
|
console.error(e.message);
|
||||||
console.log("Skip this timezone")
|
console.log("Skip this timezone")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,4 +413,3 @@ export function timezoneList() {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue