Merge branch 'eslint_stylelint'

# Conflicts:
#	server/server.js
This commit is contained in:
LouisLam 2021-07-28 20:20:10 +08:00
commit 4a9690437f
33 changed files with 1298 additions and 1141 deletions

73
.eslintrc.js Normal file
View file

@ -0,0 +1,73 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2017: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
],
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
indent: ["error", 4],
quotes: ["warn", "double"],
//semi: ['off', 'never'],
"vue/html-indent": ["warn", 4], // default: 2
"vue/max-attributes-per-line": "off",
"no-multi-spaces": ["error", {
ignoreEOLComments: true,
}],
"curly": "error",
"object-curly-spacing": ["error", "always"],
"object-curly-newline": ["error", {
"ObjectExpression": {
"minProperties": 1,
},
"ObjectPattern": {
"multiline": true,
"minProperties": 2,
},
"ImportDeclaration": {
"multiline": true,
},
"ExportDeclaration": {
"multiline": true,
//'minProperties': 2,
},
}],
"object-property-newline": "error",
"comma-spacing": "error",
"brace-style": "error",
"no-var": "error",
"key-spacing": "warn",
"keyword-spacing": "warn",
"space-infix-ops": "warn",
"arrow-spacing": "warn",
"no-trailing-spaces": "warn",
"space-before-blocks": "warn",
//'no-console': 'warn',
"no-extra-boolean-cast": "off",
"no-multiple-empty-lines": ["warn", {
"max": 1,
"maxBOF": 0,
}],
"lines-between-class-members": ["warn", "always", {
exceptAfterSingleLine: true,
}],
"no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"],
//'prefer-template': 'error',
"comma-dangle": ["warn", "always-multiline"],
},
}

3
.stylelintrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "stylelint-config-recommended",
}

View file

@ -52,11 +52,16 @@
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-legacy": "^1.4.4", "@vitejs/plugin-legacy": "^1.5.0",
"@vitejs/plugin-vue": "^1.2.5", "@vitejs/plugin-vue": "^1.3.0",
"@vue/compiler-sfc": "^3.1.5", "@vue/compiler-sfc": "^3.1.5",
"core-js": "^3.15.2", "core-js": "^3.15.2",
"sass": "^1.35.2", "eslint": "^7.31.0",
"vite": "^2.4.2" "eslint-plugin-vue": "^7.14.0",
"sass": "^1.36.0",
"stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0",
"vite": "^2.4.4"
} }
} }

View file

@ -1,6 +1,6 @@
const basicAuth = require('express-basic-auth') const basicAuth = require("express-basic-auth")
const passwordHash = require('./password-hash'); const passwordHash = require("./password-hash");
const {R} = require("redbean-node"); const { R } = require("redbean-node");
/** /**
* *
@ -10,7 +10,7 @@ const {R} = require("redbean-node");
*/ */
exports.login = async function (username, password) { exports.login = async function (username, password) {
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await R.findOne("user", " username = ? AND active = 1 ", [
username username,
]) ])
if (user && passwordHash.verify(password, user.password)) { if (user && passwordHash.verify(password, user.password)) {
@ -18,13 +18,13 @@ exports.login = async function (username, password) {
if (passwordHash.needRehash(user.password)) { if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password), passwordHash.generate(password),
user.id user.id,
]); ]);
} }
return user; return user;
} else {
return null;
} }
return null;
} }
function myAuthorizer(username, password, callback) { function myAuthorizer(username, password, callback) {
@ -36,5 +36,5 @@ function myAuthorizer(username, password, callback) {
exports.basicAuth = basicAuth({ exports.basicAuth = basicAuth({
authorizer: myAuthorizer, authorizer: myAuthorizer,
authorizeAsync: true, authorizeAsync: true,
challenge: true challenge: true,
}); });

View file

@ -1,15 +1,15 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require("dayjs/plugin/utc")
var timezone = require('dayjs/plugin/timezone') let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const {BeanModel} = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING
*/ */
class Heartbeat extends BeanModel { class Heartbeat extends BeanModel {

View file

@ -22,6 +22,7 @@ const customAgent = new https.Agent({
* status: * status:
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING
*/ */
class Monitor extends BeanModel { class Monitor extends BeanModel {
async toJSON() { async toJSON() {
@ -197,7 +198,7 @@ class Monitor extends BeanModel {
if (bean.status === UP) { if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
} else if (bean.status === PENDING) { } else if (bean.status === PENDING) {
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
} else { } else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
} }

View file

@ -1,6 +1,6 @@
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"); const child_process = require("child_process");
@ -24,7 +24,7 @@ class Notification {
params: { params: {
chat_id: notification.telegramChatID, chat_id: notification.telegramChatID,
text: msg, text: msg,
} },
}) })
return okMsg; return okMsg;
@ -41,7 +41,7 @@ class Notification {
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg, "message": msg,
"priority": notification.gotifyPriority || 8, "priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma" "title": "Uptime-Kuma",
}) })
return okMsg; return okMsg;
@ -62,10 +62,10 @@ class Notification {
if (notification.webhookContentType === "form-data") { if (notification.webhookContentType === "form-data") {
finalData = new FormData(); finalData = new FormData();
finalData.append('data', JSON.stringify(data)); finalData.append("data", JSON.stringify(data));
config = { config = {
headers: finalData.getHeaders() headers: finalData.getHeaders(),
} }
} else { } else {
@ -84,63 +84,68 @@ 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,
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
}
// 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",
content: msg embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Message",
value: msg,
},
],
}],
} }
await axios.post(notification.discordWebhookUrl, data) await axios.post(notification.discordWebhookUrl, data)
return okMsg; return okMsg;
} } catch (error) {
// If heartbeatJSON is not null, we go into the normal alerting loop. throwGeneralAxiosError(error)
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 = {};
await axios.post(notification.signalURL, data, config) await axios.post(notification.signalURL, data, config)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "slack") { } else if (notification.type === "slack") {
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo} let data = {
"text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data) await axios.post(notification.slackwebhookURL, data)
return okMsg; return okMsg;
} }
@ -148,44 +153,42 @@ class Notification {
const time = heartbeatJSON["time"]; const time = heartbeatJSON["time"];
let data = { let data = {
"text": "Uptime Kuma Alert", "text": "Uptime Kuma Alert",
"channel":notification.slackchannel, "channel": notification.slackchannel,
"username": notification.slackusername, "username": notification.slackusername,
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,
"blocks": [{ "blocks": [{
"type": "header", "type": "header",
"text": { "text": {
"type": "plain_text", "type": "plain_text",
"text": "Uptime Kuma Alert" "text": "Uptime Kuma Alert",
} },
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
}, },
{ {
"type": "section", "type": "mrkdwn",
"fields": [{ "text": "*Time (UTC)*\n" + time,
"type": "mrkdwn", }],
"text": '*Message*\n'+msg },
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
}, },
{ "value": "Uptime-Kuma",
"type": "mrkdwn", "url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
"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) await axios.post(notification.slackwebhookURL, data)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
@ -193,27 +196,35 @@ class Notification {
} }
} else if (notification.type === "pushover") { } else if (notification.type === "pushover") {
var pushoverlink = 'https://api.pushover.net/1/messages.json' let pushoverlink = "https://api.pushover.net/1/messages.json"
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>", let data = {
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds, "message": "<b>Uptime Kuma Pushover testing successful.</b>",
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1} "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) await axios.post(pushoverlink, data)
return okMsg; return okMsg;
} }
let data = { let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:"+msg+ '\n<b>Time (UTC)</b>:' +heartbeatJSON["time"], "message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user":notification.pushoveruserkey, "user": notification.pushoveruserkey,
"token": notification.pushoverapptoken, "token": notification.pushoverapptoken,
"sound": notification.pushoversounds, "sound": notification.pushoversounds,
"priority": notification.pushoverpriority, "priority": notification.pushoverpriority,
"title": notification.pushovertitle, "title": notification.pushovertitle,
"retry": "30", "retry": "30",
"expire": "3600", "expire": "3600",
"html": 1 "html": 1,
} }
await axios.post(pushoverlink, data) await axios.post(pushoverlink, data)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
@ -291,24 +302,23 @@ class Notification {
static async apprise(notification, msg) { static async apprise(notification, msg) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found';
if (output) { if (output) {
if (! output.includes("ERROR")) { if (! output.includes("ERROR")) {
return "Sent Successfully"; return "Sent Successfully";
} else {
throw new Error(output)
} }
throw new Error(output)
} else { } else {
return "" return ""
} }
} }
static checkApprise() { static checkApprise() {
let commandExistsSync = require('command-exists').sync; let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync('apprise'); let exists = commandExistsSync("apprise");
return exists; return exists;
} }

View file

@ -1,5 +1,5 @@
const passwordHashOld = require('password-hash'); const passwordHashOld = require("password-hash");
const bcrypt = require('bcrypt'); const bcrypt = require("bcrypt");
const saltRounds = 10; const saltRounds = 10;
exports.generate = function (password) { exports.generate = function (password) {
@ -9,9 +9,9 @@ exports.generate = function (password) {
exports.verify = function (password, hash) { exports.verify = function (password, hash) {
if (isSHA1(hash)) { if (isSHA1(hash)) {
return passwordHashOld.verify(password, hash) return passwordHashOld.verify(password, hash)
} else {
return bcrypt.compareSync(password, hash);
} }
return bcrypt.compareSync(password, hash);
} }
function isSHA1(hash) { function isSHA1(hash) {

View file

@ -1,9 +1,9 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js // https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows // Fixed on Windows
var spawn = require('child_process').spawn, let spawn = require("child_process").spawn,
events = require('events'), events = require("events"),
fs = require('fs'), fs = require("fs"),
WIN = /^win/.test(process.platform), WIN = /^win/.test(process.platform),
LIN = /^linux/.test(process.platform), LIN = /^linux/.test(process.platform),
MAC = /^darwin/.test(process.platform); MAC = /^darwin/.test(process.platform);
@ -11,8 +11,9 @@ var spawn = require('child_process').spawn,
module.exports = Ping; module.exports = Ping;
function Ping(host, options) { function Ping(host, options) {
if (!host) if (!host) {
throw new Error('You must specify a host to ping!'); throw new Error("You must specify a host to ping!");
}
this._host = host; this._host = host;
this._options = options = (options || {}); this._options = options = (options || {});
@ -20,26 +21,24 @@ function Ping(host, options) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
if (WIN) { if (WIN) {
this._bin = 'c:/windows/system32/ping.exe'; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} } else if (LIN) {
else if (LIN) { this._bin = "/bin/ping";
this._bin = '/bin/ping'; this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
this._args = (options.args) ? options.args : [ '-n', '-w', '2', '-c', '1', host ];
this._regmatch = /=([0-9.]+?) ms/; // need to verify this this._regmatch = /=([0-9.]+?) ms/; // need to verify this
} } else if (MAC) {
else if (MAC) { this._bin = "/sbin/ping";
this._bin = '/sbin/ping'; this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
this._args = (options.args) ? options.args : [ '-n', '-t', '2', '-c', '1', host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} } else {
else { throw new Error("Could not detect your ping binary.");
throw new Error('Could not detect your ping binary.');
} }
if (!fs.existsSync(this._bin)) if (!fs.existsSync(this._bin)) {
throw new Error('Could not detect '+this._bin+' on your system'); throw new Error("Could not detect " + this._bin + " on your system");
}
this._i = 0; this._i = 0;
@ -51,48 +50,57 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING // SEND A PING
// =========== // ===========
Ping.prototype.send = function(callback) { Ping.prototype.send = function(callback) {
var self = this; let self = this;
callback = callback || function(err, ms) { callback = callback || function(err, ms) {
if (err) return self.emit('error', err); if (err) {
else return self.emit('result', ms); return self.emit("error", err);
}
return self.emit("result", ms);
}; };
var _ended, _exited, _errored; let _ended, _exited, _errored;
this._ping = spawn(this._bin, this._args); // spawn the binary this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping.on('error', function(err) { // handle binary errors this._ping.on("error", function(err) { // handle binary errors
_errored = true; _errored = true;
callback(err); callback(err);
}); });
this._ping.stdout.on('data', function(data) { // log stdout this._ping.stdout.on("data", function(data) { // log stdout
this._stdout = (this._stdout || '') + data; this._stdout = (this._stdout || "") + data;
}); });
this._ping.stdout.on('end', function() { this._ping.stdout.on("end", function() {
_ended = true; _ended = true;
if (_exited && !_errored) onEnd.call(self._ping); if (_exited && !_errored) {
onEnd.call(self._ping);
}
}); });
this._ping.stderr.on('data', function(data) { // log stderr this._ping.stderr.on("data", function(data) { // log stderr
this._stderr = (this._stderr || '') + data; this._stderr = (this._stderr || "") + data;
}); });
this._ping.on('exit', function(code) { // handle complete this._ping.on("exit", function(code) { // handle complete
_exited = true; _exited = true;
if (_ended && !_errored) onEnd.call(self._ping); if (_ended && !_errored) {
onEnd.call(self._ping);
}
}); });
function onEnd() { function onEnd() {
var stdout = this.stdout._stdout, let stdout = this.stdout._stdout,
stderr = this.stderr._stderr, stderr = this.stderr._stderr,
ms; ms;
if (stderr) if (stderr) {
return callback(new Error(stderr)); return callback(new Error(stderr));
else if (!stdout) }
return callback(new Error('No stdout detected'));
if (!stdout) {
return callback(new Error("No stdout detected"));
}
ms = stdout.match(self._regmatch); // parse out the ##ms response ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms; ms = (ms && ms[1]) ? Number(ms[1]) : ms;
@ -104,7 +112,7 @@ Ping.prototype.send = function(callback) {
// CALL Ping#send(callback) ON A TIMER // CALL Ping#send(callback) ON A TIMER
// =================================== // ===================================
Ping.prototype.start = function(callback) { Ping.prototype.start = function(callback) {
var self = this; let self = this;
this._i = setInterval(function() { this._i = setInterval(function() {
self.send(callback); self.send(callback);
}, (self._options.interval || 5000)); }, (self._options.interval || 5000));

View file

@ -1,24 +1,24 @@
console.log("Welcome to Uptime Kuma ") console.log("Welcome to Uptime Kuma ")
console.log("Importing libraries") console.log("Importing libraries")
const express = require('express'); const express = require("express");
const http = require('http'); const http = require("http");
const { Server } = require("socket.io"); const { Server } = require("socket.io");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const {R} = require("redbean-node"); const { R } = require("redbean-node");
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 gracefulShutdown = require("http-graceful-shutdown");
const Database = require("./database"); const Database = require("./database");
const {sleep} = require("./util"); const { sleep } = require("./util");
const args = require('args-parser')(process.argv); const args = require("args-parser")(process.argv);
const prometheusAPIMetrics = require('prometheus-api-metrics'); const prometheusAPIMetrics = require("prometheus-api-metrics");
const { basicAuth } = require("./auth"); const { basicAuth } = require("./auth");
const {login} = require("./auth"); const { login } = require("./auth");
const passwordHash = require('./password-hash'); const passwordHash = require('./password-hash');
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
@ -64,12 +64,12 @@ let needSetup = false;
// Normal Router here // Normal Router here
app.use('/', express.static("dist")); app.use("/", express.static("dist"));
// Basic Auth Router here // Basic Auth Router here
// For testing // For testing
basicAuthRouter.get('/test-auth', (req, res) => { basicAuthRouter.get("/test-auth", (req, res) => {
res.end("OK") res.end("OK")
}); });
@ -78,12 +78,12 @@ let needSetup = false;
basicAuthRouter.use(prometheusAPIMetrics()) basicAuthRouter.use(prometheusAPIMetrics())
// Universal Route Handler, must be at the end // Universal Route Handler, must be at the end
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") console.log("Adding socket handler")
io.on('connection', async (socket) => { io.on("connection", async (socket) => {
socket.emit("info", { socket.emit("info", {
version, version,
@ -96,7 +96,7 @@ let needSetup = false;
socket.emit("setup") socket.emit("setup")
} }
socket.on('disconnect', () => { socket.on("disconnect", () => {
totalClient--; totalClient--;
}); });
@ -110,7 +110,7 @@ let needSetup = false;
console.log("Username from JWT: " + decoded.username) console.log("Username from JWT: " + decoded.username)
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await R.findOne("user", " username = ? AND active = 1 ", [
decoded.username decoded.username,
]) ])
if (user) { if (user) {
@ -122,13 +122,13 @@ let needSetup = false;
} else { } else {
callback({ callback({
ok: false, ok: false,
msg: "The user is inactive or deleted." msg: "The user is inactive or deleted.",
}) })
} }
} catch (error) { } catch (error) {
callback({ callback({
ok: false, ok: false,
msg: "Invalid token." msg: "Invalid token.",
}) })
} }
@ -145,13 +145,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
token: jwt.sign({ token: jwt.sign({
username: data.username username: data.username,
}, jwtSecret) }, jwtSecret),
}) })
} else { } else {
callback({ callback({
ok: false, ok: false,
msg: "Incorrect username or password." msg: "Incorrect username or password.",
}) })
} }
@ -182,13 +182,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Added Successfully." msg: "Added Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -215,13 +215,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Added Successfully.", msg: "Added Successfully.",
monitorID: bean.id monitorID: bean.id,
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -258,14 +258,14 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Saved.", msg: "Saved.",
monitorID: bean.id monitorID: bean.id,
}); });
} catch (e) { } catch (e) {
console.error(e) console.error(e)
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -289,7 +289,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -303,13 +303,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Resumed Successfully." msg: "Resumed Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -322,14 +322,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Paused Successfully." msg: "Paused Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -347,12 +346,12 @@ let needSetup = false;
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
socket.userID socket.userID,
]); ]);
callback({ callback({
ok: true, ok: true,
msg: "Deleted Successfully." msg: "Deleted Successfully.",
}); });
await sendMonitorList(socket); await sendMonitorList(socket);
@ -360,7 +359,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -374,19 +373,19 @@ let needSetup = false;
} }
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID socket.userID,
]) ])
if (user && passwordHash.verify(password.currentPassword, user.password)) { if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password.newPassword), passwordHash.generate(password.newPassword),
socket.userID socket.userID,
]); ]);
callback({ callback({
ok: true, ok: true,
msg: "Password has been updated successfully." msg: "Password has been updated successfully.",
}) })
} else { } else {
throw new Error("Incorrect current password") throw new Error("Incorrect current password")
@ -395,7 +394,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -404,7 +403,6 @@ let needSetup = false;
try { try {
checkLogin(socket) checkLogin(socket)
callback({ callback({
ok: true, ok: true,
data: await getSettings(type), data: await getSettings(type),
@ -413,7 +411,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -434,7 +432,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -454,7 +452,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -467,7 +465,7 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg msg,
}); });
} catch (e) { } catch (e) {
@ -475,7 +473,7 @@ let needSetup = false;
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@ -500,7 +498,7 @@ let needSetup = false;
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID monitorID,
]) ])
for (let notificationID in notificationIDList) { for (let notificationID in notificationIDList) {
@ -533,7 +531,7 @@ async function sendMonitorList(socket) {
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
let result = []; let result = [];
let list = await R.find("notification", " user_id = ? ", [ let list = await R.find("notification", " user_id = ? ", [
socket.userID socket.userID,
]); ]);
for (let bean of list) { for (let bean of list) {
@ -563,7 +561,7 @@ async function getMonitorJSONList(userID) {
let result = {}; let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [ let monitorList = await R.find("monitor", " user_id = ? ", [
userID userID,
]) ])
for (let monitor of monitorList) { for (let monitor of monitorList) {
@ -586,8 +584,8 @@ async function initDatabase() {
} }
console.log("Connecting to Database") console.log("Connecting to Database")
R.setup('sqlite', { R.setup("sqlite", {
filename: Database.path filename: Database.path,
}); });
console.log("Connected") console.log("Connected")
@ -599,7 +597,7 @@ async function initDatabase() {
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret" "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (! jwtSecretBean) {
@ -630,11 +628,11 @@ async function startMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
userID userID,
]); ]);
let monitor = await R.findOne("monitor", " id = ? ", [ let monitor = await R.findOne("monitor", " id = ? ", [
monitorID monitorID,
]) ])
if (monitor.id in monitorList) { if (monitor.id in monitorList) {
@ -656,7 +654,7 @@ async function pauseMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
userID userID,
]); ]);
if (monitorID in monitorList) { if (monitorID in monitorList) {
@ -685,13 +683,13 @@ async function sendHeartbeatList(socket, monitorID) {
ORDER BY time DESC ORDER BY time DESC
LIMIT 100 LIMIT 100
`, [ `, [
monitorID 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)
@ -704,23 +702,20 @@ async function sendImportantHeartbeatList(socket, monitorID) {
ORDER BY time DESC ORDER BY time DESC
LIMIT 500 LIMIT 500
`, [ `, [
monitorID monitorID,
]) ])
socket.emit("importantHeartbeatList", monitorID, list) socket.emit("importantHeartbeatList", monitorID, list)
} }
const startGracefulShutdown = async () => { const startGracefulShutdown = async () => {
console.log('Shutdown requested'); console.log("Shutdown requested");
await (new Promise((resolve) => { await (new Promise((resolve) => {
server.close(async function () { server.close(async function () {
console.log('Stopped Express.'); console.log("Stopped Express.");
process.exit(0) process.exit(0)
setTimeout(async () =>{ setTimeout(async () => {
await R.close(); await R.close();
console.log("Stopped DB") console.log("Stopped DB")
@ -730,11 +725,10 @@ const startGracefulShutdown = async () => {
}); });
})); }));
} }
async function shutdownFunction(signal) { async function shutdownFunction(signal) {
console.log('Called signal: ' + signal); console.log("Called signal: " + signal);
console.log("Stopping all monitors") console.log("Stopping all monitors")
for (let id in monitorList) { for (let id in monitorList) {
@ -746,14 +740,14 @@ async function shutdownFunction(signal) {
} }
function finalFunction() { function finalFunction() {
console.log('Graceful Shutdown') console.log("Graceful Shutdown")
} }
gracefulShutdown(server, { gracefulShutdown(server, {
signals: 'SIGINT SIGTERM', signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode development: false, // not in dev mode
forceExit: true, // triggers process.exit() at the end of shutdown process forceExit: true, // triggers process.exit() at the end of shutdown process
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ... onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction // finally function (sync) - e.g. for logging finally: finalFunction, // finally function (sync) - e.g. for logging
}); });

View file

@ -1,6 +1,6 @@
const tcpp = require('tcp-ping'); const tcpp = require("tcp-ping");
const Ping = require("./ping-lite"); const Ping = require("./ping-lite");
const {R} = require("redbean-node"); const { R } = require("redbean-node");
exports.tcping = function (hostname, port) { exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -41,13 +41,13 @@ exports.ping = function (hostname) {
exports.setting = async function (key) { exports.setting = async function (key) {
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key key,
]) ])
} }
exports.setSetting = async function (key, value) { exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key key,
]) ])
if (! bean) { if (! bean) {
bean = R.dispense("setting") bean = R.dispense("setting")
@ -59,7 +59,7 @@ exports.setSetting = async function (key, value) {
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,
]) ])
let result = {}; let result = {};
@ -71,7 +71,6 @@ exports.getSettings = async function (type) {
return result; return result;
} }
// ssl-checker by @dyaa // ssl-checker by @dyaa
// param: res - response object from axios // param: res - response object from axios
// return an object containing the certificate information // return an object containing the certificate information
@ -97,7 +96,9 @@ exports.checkCertificate = function (res) {
} = res.request.res.socket.getPeerCertificate(false); } = res.request.res.socket.getPeerCertificate(false);
if (!valid_from || !valid_to || !subjectaltname) { if (!valid_from || !valid_to || !subjectaltname) {
throw { message: 'No TLS certificate in response' }; throw {
message: "No TLS certificate in response",
};
} }
const valid = res.request.res.socket.authorized || false; const valid = res.request.res.socket.authorized || false;
@ -118,4 +119,4 @@ exports.checkCertificate = function (res) {
issuer, issuer,
fingerprint, fingerprint,
}; };
} }

View file

@ -23,4 +23,3 @@ exports.debug = (msg) => {
console.log(msg) console.log(msg)
} }
} }

View file

@ -3,11 +3,5 @@
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>
<style lang="scss">
</style>

View file

@ -1,17 +1,23 @@
<template> <template>
<div class="modal fade" tabindex="-1" ref="modal"> <div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Confirm</h5> <h5 id="exampleModalLabel" class="modal-title">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> Confirm
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot></slot> <slot />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button> <button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> Yes
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
No
</button>
</div> </div>
</div> </div>
</div> </div>
@ -19,17 +25,17 @@
</template> </template>
<script> <script>
import { Modal } from 'bootstrap' import { Modal } from "bootstrap"
export default { export default {
props: { props: {
btnStyle: { btnStyle: {
type: String, type: String,
default: "btn-primary" default: "btn-primary",
} },
}, },
data: () => ({ data: () => ({
modal: null modal: null,
}), }),
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal)
@ -39,12 +45,8 @@ export default {
this.modal.show() this.modal.show()
}, },
yes() { yes() {
this.$emit('yes'); this.$emit("yes");
} },
} },
} }
</script> </script>
<style scoped>
</style>

View file

@ -5,24 +5,20 @@
<script> <script>
import {sleep} from '../util-frontend' import { sleep } from "../util-frontend"
export default { export default {
props: { props: {
value: [String, Number], value: [String, Number],
time: { time: {
Number, type: Number,
default: 0.3, default: 0.3,
}, },
unit: { unit: {
String, type: String,
default: "ms", default: "ms",
} },
},
mounted() {
this.output = this.value;
}, },
data() { data() {
@ -32,14 +28,10 @@ export default {
} }
}, },
methods: {
},
computed: { computed: {
isNum() { isNum() {
return typeof this.value === 'number' return typeof this.value === "number"
} },
}, },
watch: { watch: {
@ -61,9 +53,11 @@ export default {
}, },
}, },
mounted() {
this.output = this.value;
},
methods: {},
} }
</script> </script>
<style scoped>
</style>

View file

@ -5,8 +5,8 @@
<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)
@ -28,14 +28,10 @@ export default {
format = "YYYY-MM-DD"; format = "YYYY-MM-DD";
} }
return dayjs.utc(this.value).tz(this.$root.timezone).format(format); return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
} else {
return "";
} }
return "";
}, },
} },
} }
</script> </script>
<style scoped>
</style>

View file

@ -1,28 +1,27 @@
<template> <template>
<div class="wrap" :style="wrapStyle" ref="wrap"> <div ref="wrap" class="wrap" :style="wrapStyle">
<div class="hp-bar-big" :style="barStyle"> <div class="hp-bar-big" :style="barStyle">
<div <div
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat" class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }" :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:style="beatStyle" :style="beatStyle"
v-for="(beat, index) in shortBeatList" :title="beat.msg"
:key="index" />
:title="beat.msg">
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
size: { size: {
type: String, type: String,
default: "big" default: "big",
}, },
monitorId: Number monitorId: Number,
}, },
data() { data() {
return { return {
@ -34,26 +33,6 @@ export default {
maxBeat: -1, maxBeat: -1,
} }
}, },
unmounted() {
window.removeEventListener("resize", this.resize);
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
}
window.addEventListener("resize", this.resize);
this.resize();
},
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
}
},
computed: { computed: {
beatList() { beatList() {
@ -80,8 +59,6 @@ export default {
start = 0; start = 0;
} }
return placeholders.concat(this.beatList.slice(start)) return placeholders.concat(this.beatList.slice(start))
}, },
@ -92,13 +69,13 @@ export default {
let width let width
if (this.maxBeat > 0) { if (this.maxBeat > 0) {
width = (this.beatWidth + this.beatMargin * 2) * this.maxBeat + (leftRight * 2) + "px" width = (this.beatWidth + this.beatMargin * 2) * this.maxBeat + (leftRight * 2) + "px"
} { } else {
width = "100%" width = "100%"
} }
return { return {
padding: `${topBottom}px ${leftRight}px`, padding: `${topBottom}px ${leftRight}px`,
width: width width: width,
} }
}, },
@ -111,11 +88,11 @@ export default {
transform: `translateX(${width}px)`, transform: `translateX(${width}px)`,
} }
} else {
return {
transform: `translateX(0)`,
}
} }
return {
transform: "translateX(0)",
}
}, },
beatStyle() { beatStyle() {
@ -125,7 +102,7 @@ export default {
margin: this.beatMargin + "px", margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale, "--hover-scale": this.hoverScale,
} }
} },
}, },
watch: { watch: {
@ -138,8 +115,28 @@ export default {
}, 300) }, 300)
}, },
deep: true, deep: true,
},
},
unmounted() {
window.removeEventListener("resize", this.resize);
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
} }
}
window.addEventListener("resize", this.resize);
this.resize();
},
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
},
},
} }
</script> </script>

View file

@ -2,31 +2,32 @@
<div class="form-container"> <div class="form-container">
<div class="form"> <div class="form">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal" />
<h1 class="h3 mb-3 fw-normal"></h1>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username"> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">Username</label> <label for="floatingInput">Username</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password"> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">Password</label> <label for="floatingPassword">Password</label>
</div> </div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4"> <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check"> <div class="form-check">
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember"> <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember"> <label class="form-check-label" for="remember">
Remember me Remember me
</label> </label>
</div> </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>
<div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok"> <div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
{{ res.msg }} {{ res.msg }}
</div> </div>
</form> </form>
@ -52,8 +53,8 @@ export default {
this.processing = false; this.processing = false;
this.res = res; this.res = res;
}) })
} },
} },
} }
</script> </script>

View file

@ -1,48 +1,70 @@
<template> <template>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5> <h5 id="exampleModalLabel" class="modal-title">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> Setup Notification
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">Notification Type</label> <label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="notification.type"> <select id="type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option> <option value="telegram">
<option value="webhook">Webhook</option> Telegram
<option value="smtp">Email (SMTP)</option> </option>
<option value="discord">Discord</option> <option value="webhook">
<option value="signal">Signal</option> Webhook
<option value="gotify">Gotify</option> </option>
<option value="slack">Slack</option> <option value="smtp">
<option value="pushover">Pushover</option> Email (SMTP)
<option value="apprise">Apprise (Support 50+ Notification services)</option> </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> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="notification.name"> <input id="name" v-model="notification.name" type="text" class="form-control" required>
</div> </div>
<template v-if="notification.type === 'telegram'"> <template v-if="notification.type === 'telegram'">
<div class="mb-3"> <div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label> <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"> <input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
<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 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>
<div class="mb-3"> <div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label> <label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID"> <input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button> <button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
Auto Get
</button>
</div> </div>
<div class="form-text"> <div class="form-text">
@ -53,7 +75,6 @@
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken"> <template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a> <a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template> </template>
@ -69,15 +90,18 @@
<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 id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
</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 id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
<option value="json">application/json</option> <option value="json">
<option value="form-data">multipart/form-data</option> application/json
</option>
<option value="form-data">
multipart/form-data
</option>
</select> </select>
<div class="form-text"> <div class="form-text">
@ -90,70 +114,71 @@
<template v-if="notification.type === 'smtp'"> <template v-if="notification.type === 'smtp'">
<div class="mb-3"> <div class="mb-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" required v-model="notification.smtpHost"> <input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input type="number" class="form-control" id="port" v-model="notification.smtpPort" required min="0" max="65535" step="1"> <input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<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 id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="secure"> <label class="form-check-label" for="secure">
Secure Secure
</label> </label>
</div> </div>
<div class="form-text">Generally, true for 465, false for other ports.</div> <div class="form-text">
Generally, true for 465, false for other ports.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Username</label> <label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" v-model="notification.smtpUsername" autocomplete="false"> <input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false"> <input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="from-email" class="form-label">From Email</label> <label for="from-email" class="form-label">From Email</label>
<input type="email" class="form-control" id="from-email" required v-model="notification.smtpFrom" autocomplete="false"> <input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="to-email" class="form-label">To Email</label> <label for="to-email" class="form-label">To Email</label>
<input type="email" class="form-control" id="to-email" required v-model="notification.smtpTo" autocomplete="false"> <input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
</div> </div>
</template> </template>
<template v-if="notification.type === 'discord'"> <template v-if="notification.type === 'discord'">
<div class="mb-3"> <div class="mb-3">
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label> <label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
<input type="text" class="form-control" id="discord-webhook-url" required v-model="notification.discordWebhookUrl" autocomplete="false"> <input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
<div class="form-text">You can get this by going to Server Settings -> Integrations -> Create Webhook</div> <div class="form-text">
You can get this by going to Server Settings -> Integrations -> Create Webhook
</div>
</div> </div>
</template> </template>
<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 id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="signal-number" class="form-label">Number</label> <label for="signal-number" class="form-label">Number</label>
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber"> <input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="signal-recipients" class="form-label">Recipients</label> <label for="signal-recipients" class="form-label">Recipients</label>
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients"> <input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
You need to have a signal client with REST API. You need to have a signal client with REST API.
@ -174,37 +199,37 @@
</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 id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
</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 id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
</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 id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" 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">Webhook URL<span style="color:red;"><sup>*</sup></span></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"> <input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label> <label for="slack-username" class="form-label">Username</label>
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername"> <input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">Icon Emoji</label> <label for="slack-iconemo" class="form-label">Icon Emoji</label>
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo"> <input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">Channel Name</label> <label for="slack-channel" class="form-label">Channel Name</label>
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel"> <input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label> <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"> <input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text"> <div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required <span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;"> <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> More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p> </p>
@ -224,15 +249,15 @@
<template v-if="notification.type === 'pushover'"> <template v-if="notification.type === 'pushover'">
<div class="mb-3"> <div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></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-user" required v-model="notification.pushoveruserkey"> <input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label> <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"> <input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
<label for="pushover-device" class="form-label">Device</label> <label for="pushover-device" class="form-label">Device</label>
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice"> <input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label> <label for="pushover-device" class="form-label">Message Title</label>
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle"> <input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
<label for="pushover-priority" class="form-label">Priority</label> <label for="pushover-priority" class="form-label">Priority</label>
<select class="form-select" id="pushover-priority" v-model="notification.pushoverpriority"> <select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
<option>-2</option> <option>-2</option>
<option>-1</option> <option>-1</option>
<option>0</option> <option>0</option>
@ -240,7 +265,7 @@
<option>2</option> <option>2</option>
</select> </select>
<label for="pushover-sound" class="form-label">Notification Sound</label> <label for="pushover-sound" class="form-label">Notification Sound</label>
<select class="form-select" id="pushover-sound" v-model="notification.pushoversounds"> <select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
<option>pushover</option> <option>pushover</option>
<option>bike</option> <option>bike</option>
<option>bugle</option> <option>bugle</option>
@ -265,16 +290,16 @@
<option>none</option> <option>none</option>
</select> </select>
<div class="form-text"> <div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required <span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a> More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour. Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
If you want to send notifications to different devices, fill out Device field. If you want to send notifications to different devices, fill out Device field.
</p> </p>
</div> </div>
</div> </div>
</template> </template>
@ -282,7 +307,7 @@
<template v-if="notification.type === 'apprise'"> <template v-if="notification.type === 'apprise'">
<div class="mb-3"> <div class="mb-3">
<label for="apprise-url" class="form-label">Apprise URL</label> <label for="apprise-url" class="form-label">Apprise URL</label>
<input type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL"> <input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p> <p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
<p> <p>
@ -293,40 +318,46 @@
<div class="mb-3"> <div class="mb-3">
<p> <p>
Status: Status:
<span class="text-primary" v-if="appriseInstalled">Apprise is installed</span> <span v-if="appriseInstalled" class="text-primary">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> <span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
</p> </p>
</div> </div>
</template> </template>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button> <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button> Delete
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button> </button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
Test
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
Save
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
<Confirm ref="confirmDelete" @yes="deleteNotification" btn-style="btn-danger">Are you sure want to delete this notification for all monitors?</Confirm> <Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
Are you sure want to delete this notification for all monitors?
</Confirm>
</template> </template>
<script> <script>
import { Modal } from 'bootstrap' import { Modal } from "bootstrap"
import { ucfirst } from '../util-frontend' 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";
const toast = useToast() const toast = useToast()
export default { export default {
components: {Confirm}, components: {
props: { Confirm,
}, },
props: {},
data() { data() {
return { return {
model: null, model: null,
@ -335,11 +366,37 @@ export default {
notification: { notification: {
name: "", name: "",
type: null, type: null,
gotifyPriority: 8 gotifyPriority: 8,
}, },
appriseInstalled: false, appriseInstalled: false,
} }
}, },
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Alert (1)`;
} else {
oldName = "";
}
if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)`
}
},
},
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal)
@ -437,35 +494,5 @@ export default {
}, },
}, },
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Alert (1)`;
} else {
oldName = "";
}
if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)`
}
}
}
} }
</script> </script>
<style scoped>
</style>

View file

@ -5,34 +5,42 @@
<script> <script>
export default { export default {
props: { props: {
status: Number status: Number,
}, },
computed: { computed: {
color() { color() {
if (this.status === 0) { if (this.status === 0) {
return "danger" return "danger"
} else if (this.status === 1) {
return "primary"
} else if (this.status === 2) {
return "warning"
} else {
return "secondary"
} }
if (this.status === 1) {
return "primary"
}
if (this.status === 2) {
return "warning"
}
return "secondary"
}, },
text() { text() {
if (this.status === 0) { if (this.status === 0) {
return "Down" return "Down"
} else if (this.status === 1) {
return "Up"
} else if (this.status === 2) {
return "Pending"
} else {
return "Unknown"
} }
if (this.status === 1) {
return "Up"
}
if (this.status === 2) {
return "Pending"
}
return "Unknown"
}, },
} },
} }
</script> </script>

View file

@ -5,10 +5,10 @@
<script> <script>
export default { export default {
props: { props: {
monitor : Object, monitor: Object,
type: String, type: String,
pill: { pill: {
Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
@ -20,44 +20,44 @@ export default {
if (this.$root.uptimeList[key] !== undefined) { if (this.$root.uptimeList[key] !== undefined) {
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%"; return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
} else {
return "N/A"
} }
return "N/A"
}, },
color() { color() {
if (this.lastHeartBeat.status === 0) { if (this.lastHeartBeat.status === 0) {
return "danger" return "danger"
} else if (this.lastHeartBeat.status === 1) {
return "primary"
} else if (this.lastHeartBeat.status === 2) {
return "warning"
} else {
return "secondary"
} }
if (this.lastHeartBeat.status === 1) {
return "primary"
}
if (this.lastHeartBeat.status === 2) {
return "warning"
}
return "secondary"
}, },
lastHeartBeat() { lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id] return this.$root.lastHeartbeatList[this.monitor.id]
} else { }
return { status: -1 }
return {
status: -1,
} }
}, },
className() { className() {
if (this.pill) { if (this.pill) {
return `badge rounded-pill bg-${this.color}`; return `badge rounded-pill bg-${this.color}`;
} else {
return "";
} }
return "";
}, },
}, },
} }
</script> </script>
<style scoped>
</style>

View file

@ -1,12 +1,10 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from "@fortawesome/fontawesome-svg-core"
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
//import { fa } from '@fortawesome/free-regular-svg-icons' //import { fa } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
import { faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList } from '@fortawesome/free-solid-svg-icons'
// Add Free Font Awesome Icons here // Add Free Font Awesome Icons here
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free // https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList) library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
export { export { FontAwesomeIcon }
FontAwesomeIcon
}

View file

@ -3,11 +3,5 @@
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>
<style scoped>
</style>

View file

@ -1,28 +1,35 @@
<template> <template>
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
<div class="container-fluid"> <div class="container-fluid">
Lost connection to the socket server. Reconnecting... Lost connection to the socket server. Reconnecting...
</div> </div>
</div> </div>
<!-- Desktop header --> <!-- Desktop header -->
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile"> <header 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"> <router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo"></object> <object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
<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"><router-link to="/dashboard" class="nav-link"><font-awesome-icon icon="tachometer-alt" /> Dashboard</router-link></li> <li class="nav-item">
<li class="nav-item"><router-link to="/settings" class="nav-link"><font-awesome-icon icon="cog" /> Settings</router-link></li> <router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> Dashboard
</router-link>
</li>
<li class="nav-item">
<router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> 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 v-else class="d-flex flex-wrap justify-content-center mt-3 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none"> <router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg"></object> <object class="bi" width="40" height="40" data="/icon.svg" />
<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>
@ -42,9 +49,8 @@
</footer> </footer>
<!-- Mobile Only --> <!-- Mobile Only -->
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div> <div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
<nav class="bottom-nav" v-if="$root.isMobile"> <nav v-if="$root.isMobile" class="bottom-nav">
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"> <router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="tachometer-alt" /></div> <div><font-awesome-icon icon="tachometer-alt" /></div>
Dashboard Dashboard
@ -64,7 +70,6 @@
<div><font-awesome-icon icon="cog" /></div> <div><font-awesome-icon icon="cog" /></div>
Settings Settings
</router-link> </router-link>
</nav> </nav>
</template> </template>
@ -73,23 +78,19 @@ import Login from "../components/Login.vue";
export default { export default {
components: { components: {
Login Login,
}, },
data() { data() {
return { return {}
}
},
computed: {
},
mounted() {
this.init();
}, },
computed: {},
watch: { watch: {
$route (to, from) { $route (to, from) {
this.init(); this.init();
} },
},
mounted() {
this.init();
}, },
methods: { methods: {
init() { init() {
@ -98,7 +99,7 @@ export default {
} }
}, },
} },
} }
</script> </script>
@ -154,10 +155,6 @@ export default {
color: white; color: white;
} }
main {
}
footer { footer {
color: #AAA; color: #AAA;
font-size: 13px; font-size: 13px;

View file

@ -1,59 +1,58 @@
import {createApp, h} from "vue"; import "bootstrap";
import {createRouter, createWebHistory} from 'vue-router' import { createApp, h } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from './App.vue' import Toast from "vue-toastification";
import Layout from './layouts/Layout.vue' import "vue-toastification/dist/index.css";
import EmptyLayout from './layouts/EmptyLayout.vue' import App from "./App.vue";
import Settings from "./pages/Settings.vue"; import "./assets/app.scss";
import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue";
import socket from "./mixins/socket";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue"; import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue"; import Details from "./pages/Details.vue";
import socket from "./mixins/socket"
import "./assets/app.scss"
import EditMonitor from "./pages/EditMonitor.vue"; import EditMonitor from "./pages/EditMonitor.vue";
import Toast from "vue-toastification"; import Settings from "./pages/Settings.vue";
import "vue-toastification/dist/index.css";
import "bootstrap"
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
import {FontAwesomeIcon} from "./icon.js"
const routes = [ const routes = [
{ {
path: '/', path: "/",
component: Layout, component: Layout,
children: [ children: [
{ {
name: "root", name: "root",
path: '', path: "",
component: Dashboard, component: Dashboard,
children: [ children: [
{ {
name: "DashboardHome", name: "DashboardHome",
path: '/dashboard', path: "/dashboard",
component: DashboardHome, component: DashboardHome,
children: [ children: [
{ {
path: '/dashboard/:id', path: "/dashboard/:id",
component: EmptyLayout, component: EmptyLayout,
children: [ children: [
{ {
path: '', path: "",
component: Details, component: Details,
}, },
{ {
path: '/edit/:id', path: "/edit/:id",
component: EditMonitor, component: EditMonitor,
}, },
] ],
}, },
{ {
path: '/add', path: "/add",
component: EditMonitor, component: EditMonitor,
}, },
] ],
}, },
{ {
path: '/settings', path: "/settings",
component: Settings, component: Settings,
}, },
], ],
@ -63,13 +62,13 @@ const routes = [
}, },
{ {
path: '/setup', path: "/setup",
component: Setup, component: Setup,
}, },
] ]
const router = createRouter({ const router = createRouter({
linkActiveClass: 'active', linkActiveClass: "active",
history: createWebHistory(), history: createWebHistory(),
routes, routes,
}) })
@ -78,18 +77,17 @@ const app = createApp({
mixins: [ mixins: [
socket, socket,
], ],
render: ()=>h(App) render: () => h(App),
}) })
app.use(router) app.use(router)
const options = { const options = {
position: "bottom-right" position: "bottom-right",
}; };
app.use(Toast, options); app.use(Toast, options);
app.component('font-awesome-icon', FontAwesomeIcon) app.component("FontAwesomeIcon", FontAwesomeIcon)
app.mount('#app')
app.mount("#app")

View file

@ -1,6 +1,6 @@
import {io} from "socket.io-client";
import { useToast } from 'vue-toastification'
import dayjs from "dayjs"; import dayjs from "dayjs";
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast()
let socket; let socket;
@ -33,7 +33,7 @@ export default {
}, },
created() { created() {
window.addEventListener('resize', this.onResize); window.addEventListener("resize", this.onResize);
let wsHost; let wsHost;
const env = process.env.NODE_ENV || "production"; const env = process.env.NODE_ENV || "production";
@ -44,18 +44,18 @@ export default {
} }
socket = io(wsHost, { socket = io(wsHost, {
transports: ['websocket'] transports: ["websocket"],
}); });
socket.on("connect_error", (err) => { socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); 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;
}); });
socket.on('setup', (monitorID, data) => { socket.on("setup", (monitorID, data) => {
this.$router.push("/setup") this.$router.push("/setup")
}); });
@ -73,11 +73,11 @@ export default {
this.monitorList = data; this.monitorList = data;
}); });
socket.on('notificationList', (data) => { socket.on("notificationList", (data) => {
this.notificationList = data; this.notificationList = data;
}); });
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] = [];
} }
@ -100,7 +100,6 @@ export default {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
} }
if (! (data.monitorID in this.importantHeartbeatList)) { if (! (data.monitorID in this.importantHeartbeatList)) {
this.importantHeartbeatList[data.monitorID] = []; this.importantHeartbeatList[data.monitorID] = [];
} }
@ -109,7 +108,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 {
@ -117,19 +116,19 @@ export default {
} }
}); });
socket.on('avgPing', (monitorID, data) => { socket.on("avgPing", (monitorID, data) => {
this.avgPingList[monitorID] = data this.avgPingList[monitorID] = data
}); });
socket.on('uptime', (monitorID, type, data) => { socket.on("uptime", (monitorID, type, data) => {
this.uptimeList[`${monitorID}_${type}`] = data this.uptimeList[`${monitorID}_${type}`] = data
}); });
socket.on('certInfo', (monitorID, data) => { socket.on("certInfo", (monitorID, data) => {
this.certInfoList[monitorID] = JSON.parse(data) this.certInfoList[monitorID] = JSON.parse(data)
}); });
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 {
@ -137,12 +136,12 @@ export default {
} }
}); });
socket.on('disconnect', () => { socket.on("disconnect", () => {
console.log("disconnect") console.log("disconnect")
this.socket.connected = false; this.socket.connected = false;
}); });
socket.on('connect', () => { socket.on("connect", () => {
console.log("connect") console.log("connect")
this.socket.connectCount++; this.socket.connectCount++;
this.socket.connected = true; this.socket.connected = true;
@ -201,7 +200,7 @@ export default {
this.loggedIn = true; this.loggedIn = true;
// Trigger Chrome Save Password // Trigger Chrome Save Password
history.pushState({}, '') history.pushState({}, "")
} }
callback(res) callback(res)
@ -254,10 +253,9 @@ export default {
if (this.userTimezone === "auto") { if (this.userTimezone === "auto") {
return dayjs.tz.guess() return dayjs.tz.guess()
} else {
return this.userTimezone
} }
return this.userTimezone
}, },
lastHeartbeatList() { lastHeartbeatList() {
@ -276,7 +274,7 @@ export default {
let unknown = { let unknown = {
text: "Unknown", text: "Unknown",
color: "secondary" color: "secondary",
} }
for (let monitorID in this.lastHeartbeatList) { for (let monitorID in this.lastHeartbeatList) {
@ -287,17 +285,17 @@ export default {
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === 1) {
result[monitorID] = { result[monitorID] = {
text: "Up", text: "Up",
color: "primary" color: "primary",
}; };
} else if (lastHeartBeat.status === 0) { } else if (lastHeartBeat.status === 0) {
result[monitorID] = { result[monitorID] = {
text: "Down", text: "Down",
color: "danger" color: "danger",
}; };
} else if (lastHeartBeat.status === 2) { } else if (lastHeartBeat.status === 2) {
result[monitorID] = { result[monitorID] = {
text: "Pending", text: "Pending",
color: "warning" color: "warning",
}; };
} else { } else {
result[monitorID] = unknown; result[monitorID] = unknown;
@ -305,23 +303,22 @@ export default {
} }
return result; return result;
} },
}, },
watch: { watch: {
// Reload the SPA if the server version is changed. // Reload the SPA if the server version is changed.
"info.version"(to, from) { "info.version"(to, from) {
if (from && from !== to) { if (from && from !== to) {
window.location.reload() window.location.reload()
} }
}, },
remember() { remember() {
localStorage.remember = (this.remember) ? "1" : "0" localStorage.remember = (this.remember) ? "1" : "0"
} },
} },
} }

View file

@ -1,36 +1,33 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-xl-4"> <div class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile"> <div v-if="! $root.isMobile">
<router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link> <router-link to="/add" class="btn btn-primary">
<font-awesome-icon icon="plus" /> Add New Monitor
</router-link>
</div> </div>
<div class="shadow-box list mb-4" v-if="showList"> <div v-if="showList" class="shadow-box list mb-4">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
<div class="text-center mt-3" v-if="Object.keys($root.monitorList).length === 0"> No Monitors, please <router-link to="/add">
No Monitors, please <router-link to="/add">add one</router-link>. add one
</router-link>.
</div> </div>
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index"> <router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" @click="$root.cancelActiveList">
<div class="row"> <div class="row">
<div class="col-6 col-md-8 small-padding"> <div class="col-6 col-md-8 small-padding">
<div class="info"> <div class="info">
<Uptime :monitor="item" type="24" :pill="true" /> <Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }} {{ item.name }}
</div> </div>
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" /> <HeartbeatBar size="small" :monitor-id="item.id" />
</div> </div>
</div> </div>
</router-link> </router-link>
</div> </div>
</div> </div>
<div class="col-12 col-md-7 col-xl-8"> <div class="col-12 col-md-7 col-xl-8">
@ -38,7 +35,6 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -49,12 +45,10 @@ import Uptime from "../components/Uptime.vue";
export default { export default {
components: { components: {
Uptime, Uptime,
HeartbeatBar HeartbeatBar,
}, },
data() { data() {
return { return {}
}
}, },
computed: { computed: {
sortedMonitorList() { sortedMonitorList() {
@ -94,8 +88,8 @@ export default {
methods: { methods: {
monitorURL(id) { monitorURL(id) {
return "/dashboard/" + id; return "/dashboard/" + id;
} },
} },
} }
</script> </script>

View file

@ -1,15 +1,16 @@
<template> <template>
<div v-if="$route.name === 'DashboardHome'"> <div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">Quick Stats</h1> <h1 class="mb-3">
Quick Stats
</h1>
<div class="shadow-box big-padding text-center"> <div class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>Up</h3> <h3>Up</h3>
<span class="num">{{ stats.up }}</span> <span class="num">{{ stats.up }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Down</h3> <h3>Down</h3>
<span class="num text-danger">{{ stats.down }}</span> <span class="num text-danger">{{ stats.down }}</span>
</div> </div>
@ -22,16 +23,16 @@
<span class="num text-secondary">{{ stats.pause }}</span> <span class="num text-secondary">{{ stats.pause }}</span>
</div> </div>
</div> </div>
<div class="row" v-if="false"> <div v-if="false" class="row">
<div class="col-3"> <div class="col-3">
<h3>Uptime</h3> <h3>Uptime</h3>
<p>(24-hour)</p> <p>(24-hour)</p>
<span class="num"></span> <span class="num" />
</div> </div>
<div class="col-3"> <div class="col-3">
<h3>Uptime</h3> <h3>Uptime</h3>
<p>(30-day)</p> <p>(30-day)</p>
<span class="num"></span> <span class="num" />
</div> </div>
</div> </div>
</div> </div>
@ -39,32 +40,35 @@
<div class="shadow-box" style="margin-top: 25px;"> <div class="shadow-box" style="margin-top: 25px;">
<table class="table table-borderless table-hover"> <table class="table table-borderless table-hover">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>DateTime</th> <th>DateTime</th>
<th>Message</th> <th>Message</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index"> <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>
<td>{{ beat.msg }}</td> <td>{{ beat.msg }}</td>
</tr> </tr>
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="4">No important events</td> <td colspan="4">
</tr> No important events
</td>
</tr>
</tbody> </tbody>
</table> </table>
<div class="d-flex justify-content-center kuma_pagination"> <div class="d-flex justify-content-center kuma_pagination">
<pagination <pagination
v-model="page" v-model="page"
:records=importantHeartBeatList.length :records="importantHeartBeatList.length"
:per-page="perPage" /> :per-page="perPage"
/>
</div> </div>
</div> </div>
</div> </div>
@ -111,7 +115,7 @@ export default {
} else if (beat.status === 0) { } else if (beat.status === 0) {
result.down++; result.down++;
} else if (beat.status === 2) { } else if (beat.status === 2) {
result.up++; result.up++;
} else { } else {
result.unknown++; result.unknown++;
} }
@ -127,7 +131,7 @@ export default {
let result = []; let result = [];
for (let monitorID in this.$root.importantHeartbeatList) { for (let monitorID in this.$root.importantHeartbeatList) {
let list = this.$root.importantHeartbeatList[monitorID] let list = this.$root.importantHeartbeatList[monitorID]
result = result.concat(list); result = result.concat(list);
} }
@ -142,11 +146,13 @@ export default {
result.sort((a, b) => { result.sort((a, b) => {
if (a.time > b.time) { if (a.time > b.time) {
return -1; return -1;
} else if (a.time < b.time) {
return 1;
} else {
return 0;
} }
if (a.time < b.time) {
return 1;
}
return 0;
}); });
this.heartBeatList = result; this.heartBeatList = result;
@ -159,7 +165,7 @@ export default {
const endIndex = startIndex + this.perPage; const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex); return this.heartBeatList.slice(startIndex, endIndex);
}, },
} },
} }
</script> </script>

View file

@ -1,20 +1,28 @@
<template> <template>
<h1> {{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<p class="url"> <p class="url">
<a :href="monitor.url" target="_blank" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">{{ monitor.url }}</a> <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span> <span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
<br /> <br>
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span> <span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
</span> </span>
</p> </p>
<div class="functions"> <div class="functions">
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active"><font-awesome-icon icon="pause" /> Pause</button> <button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active"><font-awesome-icon icon="pause" /> Resume</button> <font-awesome-icon icon="pause" /> Pause
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary"><font-awesome-icon icon="edit" /> Edit</router-link> </button>
<button class="btn btn-danger" @click="deleteDialog"><font-awesome-icon icon="trash" /> Delete</button> <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<font-awesome-icon icon="pause" /> Resume
</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
<font-awesome-icon icon="edit" /> Edit
</router-link>
<button class="btn btn-danger" @click="deleteDialog">
<font-awesome-icon icon="trash" /> Delete
</button>
</div> </div>
<div class="shadow-box"> <div class="shadow-box">
@ -37,7 +45,7 @@
<span class="num"><CountUp :value="ping" /></span> <span class="num"><CountUp :value="ping" /></span>
</div> </div>
<div class="col"> <div class="col">
<h4>Avg.{{ pingTitle }}</h4> <h4>Avg. {{ pingTitle }}</h4>
<p>(24-hour)</p> <p>(24-hour)</p>
<span class="num"><CountUp :value="avgPing" /></span> <span class="num"><CountUp :value="avgPing" /></span>
</div> </div>
@ -52,40 +60,50 @@
<span class="num"><Uptime :monitor="monitor" type="720" /></span> <span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div> </div>
<div class="col" v-if="certInfo"> <div v-if="certInfo" class="col">
<h4>CertExp.</h4> <h4>Cert Exp.</h4>
<p>(<Datetime :value="certInfo.validTo" date-only />)</p> <p>(<Datetime :value="certInfo.validTo" date-only />)</p>
<span class="num" > <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{certInfo.daysRemaining}} days</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="shadow-box big-padding text-center" v-if="showCertInfoBox"> <div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4>Certificate Info</h4> <h4>Certificate Info</h4>
<table class="text-start"> <table class="text-start">
<tbody> <tbody>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Valid: </td> <td class="px-3">
Valid:
</td>
<td>{{ certInfo.valid }}</td> <td>{{ certInfo.valid }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Valid To: </td> <td class="px-3">
Valid To:
</td>
<td><Datetime :value="certInfo.validTo" /></td> <td><Datetime :value="certInfo.validTo" /></td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Days Remaining: </td> <td class="px-3">
Days Remaining:
</td>
<td>{{ certInfo.daysRemaining }}</td> <td>{{ certInfo.daysRemaining }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Issuer: </td> <td class="px-3">
Issuer:
</td>
<td>{{ certInfo.issuer }}</td> <td>{{ certInfo.issuer }}</td>
</tr> </tr>
<tr class="my-3"> <tr class="my-3">
<td class="px-3">Fingerprint: </td> <td class="px-3">
Fingerprint:
</td>
<td>{{ certInfo.fingerprint }}</td> <td>{{ certInfo.fingerprint }}</td>
</tr> </tr>
</tbody> </tbody>
@ -111,7 +129,9 @@
</tr> </tr>
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">No important events</td> <td colspan="3">
No important events
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -119,8 +139,9 @@
<div class="d-flex justify-content-center kuma_pagination"> <div class="d-flex justify-content-center kuma_pagination">
<pagination <pagination
v-model="page" v-model="page"
:records=importantHeartBeatList.length :records="importantHeartBeatList.length"
:per-page="perPage" /> :per-page="perPage"
/>
</div> </div>
</div> </div>
@ -128,13 +149,13 @@
Are you sure want to pause? Are you sure want to pause?
</Confirm> </Confirm>
<Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor"> <Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
Are you sure want to delete this monitor? Are you sure want to delete this monitor?
</Confirm> </Confirm>
</template> </template>
<script> <script>
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
import HeartbeatBar from "../components/HeartbeatBar.vue"; import HeartbeatBar from "../components/HeartbeatBar.vue";
@ -153,9 +174,6 @@ export default {
Confirm, Confirm,
Status, Status,
Pagination, Pagination,
},
mounted() {
}, },
data() { data() {
return { return {
@ -170,9 +188,9 @@ export default {
pingTitle() { pingTitle() {
if (this.monitor.type === "http") { if (this.monitor.type === "http") {
return "Response" return "Response"
} else {
return "Ping"
} }
return "Ping"
}, },
monitor() { monitor() {
@ -183,50 +201,52 @@ export default {
lastHeartBeat() { lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id] return this.$root.lastHeartbeatList[this.monitor.id]
} else { }
return { status: -1 }
return {
status: -1,
} }
}, },
ping() { ping() {
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) { if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
return this.lastHeartBeat.ping; return this.lastHeartBeat.ping;
} else {
return "N/A"
} }
return "N/A"
}, },
avgPing() { avgPing() {
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) { if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
return this.$root.avgPingList[this.monitor.id]; return this.$root.avgPingList[this.monitor.id];
} else {
return "N/A"
} }
return "N/A"
}, },
importantHeartBeatList() { importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) { if (this.$root.importantHeartbeatList[this.monitor.id]) {
this.heartBeatList = 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 {
return [];
} }
return [];
}, },
status() { status() {
if (this.$root.statusList[this.monitor.id]) { if (this.$root.statusList[this.monitor.id]) {
return this.$root.statusList[this.monitor.id] return this.$root.statusList[this.monitor.id]
} else {
return { }
} }
return { }
}, },
certInfo() { certInfo() {
if (this.$root.certInfoList[this.monitor.id]) { if (this.$root.certInfoList[this.monitor.id]) {
return this.$root.certInfoList[this.monitor.id] return this.$root.certInfoList[this.monitor.id]
} else {
return null
} }
return null
}, },
showCertInfoBox() { showCertInfoBox() {
@ -238,6 +258,9 @@ export default {
const endIndex = startIndex + this.perPage; const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex); return this.heartBeatList.slice(startIndex, endIndex);
}, },
},
mounted() {
}, },
methods: { methods: {
testNotification() { testNotification() {
@ -274,9 +297,9 @@ export default {
toast.error(res.msg); toast.error(res.msg);
} }
}) })
} },
} },
} }
</script> </script>

View file

@ -1,85 +1,102 @@
<template> <template>
<h1 class="mb-3">{{ pageName }}</h1> <h1 class="mb-3">
{{ pageName }}
</h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="shadow-box">
<div class="shadow-box"> <div class="row">
<div class="row"> <div class="col-md-6">
<div class="col-md-6"> <h2>General</h2>
<h2>General</h2>
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">Monitor Type</label>
<select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http">HTTP(s)</option> <option value="http">
<option value="port">TCP Port</option> HTTP(s)
<option value="ping">Ping</option> </option>
<option value="keyword">HTTP(s) - Keyword</option> <option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - Keyword
</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" v-model="monitor.name" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'http' || monitor.type === 'keyword' "> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'keyword' "> <div v-if="monitor.type === 'keyword' " class="mb-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">Keyword</label>
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">Search keyword in plain html or JSON response and it is case-sensitive</div> <div class="form-text">
Search keyword in plain html or JSON response and it is case-sensitive
</div>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' "> <div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" v-model="monitor.hostname" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'port' "> <div v-if="monitor.type === 'port' " class="mb-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input type="number" class="form-control" id="port" v-model="monitor.port" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<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="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="maxRetries" class="form-label">Retries</label> <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"> <input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" 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 class="form-text">
Maximum retries before the service is marked as down and a notification is sent
</div>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button> <button class="btn btn-primary" type="submit" :disabled="processing">
Save
</button>
</div> </div>
</div>
<div class="col-md-6">
<div class="mt-3" v-if="$root.isMobile"></div>
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
<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]">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div> </div>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div> </div>
</div> </div>
</div>
</form> </form>
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
@ -87,15 +104,12 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
},
mounted() {
this.init();
}, },
data() { data() {
return { return {
@ -114,7 +128,15 @@ export default {
}, },
isEdit() { isEdit() {
return this.$route.path.startsWith("/edit"); return this.$route.path.startsWith("/edit");
} },
},
watch: {
"$route.fullPath" () {
this.init();
},
},
mounted() {
this.init();
}, },
methods: { methods: {
init() { init() {
@ -161,12 +183,7 @@ export default {
this.$root.toastRes(res) this.$root.toastRes(res)
}) })
} }
} },
},
watch: {
'$route.fullPath' () {
this.init();
}
}, },
} }
</script> </script>

View file

@ -1,22 +1,29 @@
<template> <template>
<h1 class="mb-3">Settings</h1> <h1 class="mb-3">
Settings
</h1>
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2>General</h2>
<form class="mb-3" @submit.prevent="saveGeneral"> <form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3"> <div class="mb-3">
<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 id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">Auto: {{ guessTimezone }}</option> <option value="auto">
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option> Auto: {{ guessTimezone }}
</option>
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
{{ timezone.name }}
</option>
</select> </select>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit">Save</button> <button class="btn btn-primary" type="submit">
Save
</button>
</div> </div>
</form> </form>
@ -24,51 +31,58 @@
<form class="mb-3" @submit.prevent="savePassword"> <form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3"> <div class="mb-3">
<label for="current-password" class="form-label">Current Password</label> <label for="current-password" class="form-label">Current Password</label>
<input type="password" class="form-control" id="current-password" required v-model="password.currentPassword"> <input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="new-password" class="form-label">New Password</label> <label for="new-password" class="form-label">New Password</label>
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword"> <input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<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 id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
The repeat password does not match. The repeat password does not match.
</div> </div>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit">Update Password</button> <button class="btn btn-primary" type="submit">
Update Password
</button>
</div> </div>
</form> </form>
<div> <div>
<button class="btn btn-danger" @click="$root.logout">Logout</button> <button class="btn btn-danger" @click="$root.logout">
Logout
</button>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<div class="mt-3" v-if="$root.isMobile"></div>
<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">
<p v-else>Please assign a notification to monitor(s) to get it to work.</p> Not available, please setup.
</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, index) in $root.notificationList" :key="index"> <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ 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>
</ul> </ul>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button> <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div> </div>
</div> </div>
</div> </div>
@ -77,18 +91,18 @@
<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)
import {timezoneList} from "../util-frontend"; import { timezoneList } from "../util-frontend";
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
}, },
data() { data() {
return { return {
@ -100,9 +114,14 @@ export default {
currentPassword: "", currentPassword: "",
newPassword: "", newPassword: "",
repeatNewPassword: "", repeatNewPassword: "",
} },
} }
}, },
watch: {
"password.repeatNewPassword"() {
this.invalidPassword = false;
},
},
mounted() { mounted() {
@ -130,11 +149,6 @@ export default {
} }
}, },
}, },
watch: {
"password.repeatNewPassword"() {
this.invalidPassword = false;
}
}
} }
</script> </script>

View file

@ -2,38 +2,42 @@
<div class="form-container"> <div class="form-container">
<div class="form"> <div class="form">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div> <div>
<object width="64" height="64" data="/icon.svg"></object> <object width="64" height="64" data="/icon.svg" />
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">Uptime Kuma</div> <div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
Uptime Kuma
</div>
</div> </div>
<p class="mt-3">Create your admin account</p> <p class="mt-3">
Create your admin account
</p>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username" required> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
<label for="floatingInput">Username</label> <label for="floatingInput">Username</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password" required> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
<label for="floatingPassword">Password</label> <label for="floatingPassword">Password</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="repeat" placeholder="Repeat Password" v-model="repeatPassword" required> <input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
<label for="repeat">Repeat Password</label> <label for="repeat">Repeat Password</label>
</div> </div>
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">Create</button> <button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
Create
</button>
</form> </form>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
@ -70,8 +74,8 @@ export default {
this.$router.push("/") this.$router.push("/")
} }
}) })
} },
} },
} }
</script> </script>

View file

@ -1,6 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc' import timezone from "dayjs/plugin/timezone";
import timezone from 'dayjs/plugin/timezone' import utc from "dayjs/plugin/utc";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -18,11 +18,12 @@ export function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1); return firstLetter.toUpperCase() + str.substr(1);
} }
function getTimezoneOffset(timeZone) { function getTimezoneOffset(timeZone) {
const now = new Date(); const now = new Date();
const tzString = now.toLocaleString('en-US', { timeZone }); const tzString = now.toLocaleString("en-US", {
const localString = now.toLocaleString('en-US'); timeZone,
});
const localString = now.toLocaleString("en-US");
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000; const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
const offset = diff + now.getTimezoneOffset() / 60; const offset = diff + now.getTimezoneOffset() / 60;
return -offset; return -offset;
@ -31,355 +32,354 @@ 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 // TODO: Move to separate file
const aryIannaTimeZones = [ const aryIannaTimeZones = [
'Europe/Andorra', "Europe/Andorra",
'Asia/Dubai', "Asia/Dubai",
'Asia/Kabul', "Asia/Kabul",
'Europe/Tirane', "Europe/Tirane",
'Asia/Yerevan', "Asia/Yerevan",
'Antarctica/Casey', "Antarctica/Casey",
'Antarctica/Davis', "Antarctica/Davis",
'Antarctica/Mawson', "Antarctica/Mawson",
'Antarctica/Palmer', "Antarctica/Palmer",
'Antarctica/Rothera', "Antarctica/Rothera",
'Antarctica/Syowa', "Antarctica/Syowa",
'Antarctica/Troll', "Antarctica/Troll",
'Antarctica/Vostok', "Antarctica/Vostok",
'America/Argentina/Buenos_Aires', "America/Argentina/Buenos_Aires",
'America/Argentina/Cordoba', "America/Argentina/Cordoba",
'America/Argentina/Salta', "America/Argentina/Salta",
'America/Argentina/Jujuy', "America/Argentina/Jujuy",
'America/Argentina/Tucuman', "America/Argentina/Tucuman",
'America/Argentina/Catamarca', "America/Argentina/Catamarca",
'America/Argentina/La_Rioja', "America/Argentina/La_Rioja",
'America/Argentina/San_Juan', "America/Argentina/San_Juan",
'America/Argentina/Mendoza', "America/Argentina/Mendoza",
'America/Argentina/San_Luis', "America/Argentina/San_Luis",
'America/Argentina/Rio_Gallegos', "America/Argentina/Rio_Gallegos",
'America/Argentina/Ushuaia', "America/Argentina/Ushuaia",
'Pacific/Pago_Pago', "Pacific/Pago_Pago",
'Europe/Vienna', "Europe/Vienna",
'Australia/Lord_Howe', "Australia/Lord_Howe",
'Antarctica/Macquarie', "Antarctica/Macquarie",
'Australia/Hobart', "Australia/Hobart",
'Australia/Currie', "Australia/Currie",
'Australia/Melbourne', "Australia/Melbourne",
'Australia/Sydney', "Australia/Sydney",
'Australia/Broken_Hill', "Australia/Broken_Hill",
'Australia/Brisbane', "Australia/Brisbane",
'Australia/Lindeman', "Australia/Lindeman",
'Australia/Adelaide', "Australia/Adelaide",
'Australia/Darwin', "Australia/Darwin",
'Australia/Perth', "Australia/Perth",
'Australia/Eucla', "Australia/Eucla",
'Asia/Baku', "Asia/Baku",
'America/Barbados', "America/Barbados",
'Asia/Dhaka', "Asia/Dhaka",
'Europe/Brussels', "Europe/Brussels",
'Europe/Sofia', "Europe/Sofia",
'Atlantic/Bermuda', "Atlantic/Bermuda",
'Asia/Brunei', "Asia/Brunei",
'America/La_Paz', "America/La_Paz",
'America/Noronha', "America/Noronha",
'America/Belem', "America/Belem",
'America/Fortaleza', "America/Fortaleza",
'America/Recife', "America/Recife",
'America/Araguaina', "America/Araguaina",
'America/Maceio', "America/Maceio",
'America/Bahia', "America/Bahia",
'America/Sao_Paulo', "America/Sao_Paulo",
'America/Campo_Grande', "America/Campo_Grande",
'America/Cuiaba', "America/Cuiaba",
'America/Santarem', "America/Santarem",
'America/Porto_Velho', "America/Porto_Velho",
'America/Boa_Vista', "America/Boa_Vista",
'America/Manaus', "America/Manaus",
'America/Eirunepe', "America/Eirunepe",
'America/Rio_Branco', "America/Rio_Branco",
'America/Nassau', "America/Nassau",
'Asia/Thimphu', "Asia/Thimphu",
'Europe/Minsk', "Europe/Minsk",
'America/Belize', "America/Belize",
'America/St_Johns', "America/St_Johns",
'America/Halifax', "America/Halifax",
'America/Glace_Bay', "America/Glace_Bay",
'America/Moncton', "America/Moncton",
'America/Goose_Bay', "America/Goose_Bay",
'America/Blanc-Sablon', "America/Blanc-Sablon",
'America/Toronto', "America/Toronto",
'America/Nipigon', "America/Nipigon",
'America/Thunder_Bay', "America/Thunder_Bay",
'America/Iqaluit', "America/Iqaluit",
'America/Pangnirtung', "America/Pangnirtung",
'America/Atikokan', "America/Atikokan",
'America/Winnipeg', "America/Winnipeg",
'America/Rainy_River', "America/Rainy_River",
'America/Resolute', "America/Resolute",
'America/Rankin_Inlet', "America/Rankin_Inlet",
'America/Regina', "America/Regina",
'America/Swift_Current', "America/Swift_Current",
'America/Edmonton', "America/Edmonton",
'America/Cambridge_Bay', "America/Cambridge_Bay",
'America/Yellowknife', "America/Yellowknife",
'America/Inuvik', "America/Inuvik",
'America/Creston', "America/Creston",
'America/Dawson_Creek', "America/Dawson_Creek",
'America/Fort_Nelson', "America/Fort_Nelson",
'America/Vancouver', "America/Vancouver",
'America/Whitehorse', "America/Whitehorse",
'America/Dawson', "America/Dawson",
'Indian/Cocos', "Indian/Cocos",
'Europe/Zurich', "Europe/Zurich",
'Africa/Abidjan', "Africa/Abidjan",
'Pacific/Rarotonga', "Pacific/Rarotonga",
'America/Santiago', "America/Santiago",
'America/Punta_Arenas', "America/Punta_Arenas",
'Pacific/Easter', "Pacific/Easter",
'Asia/Shanghai', "Asia/Shanghai",
'Asia/Urumqi', "Asia/Urumqi",
'America/Bogota', "America/Bogota",
'America/Costa_Rica', "America/Costa_Rica",
'America/Havana', "America/Havana",
'Atlantic/Cape_Verde', "Atlantic/Cape_Verde",
'America/Curacao', "America/Curacao",
'Indian/Christmas', "Indian/Christmas",
'Asia/Nicosia', "Asia/Nicosia",
'Asia/Famagusta', "Asia/Famagusta",
'Europe/Prague', "Europe/Prague",
'Europe/Berlin', "Europe/Berlin",
'Europe/Copenhagen', "Europe/Copenhagen",
'America/Santo_Domingo', "America/Santo_Domingo",
'Africa/Algiers', "Africa/Algiers",
'America/Guayaquil', "America/Guayaquil",
'Pacific/Galapagos', "Pacific/Galapagos",
'Europe/Tallinn', "Europe/Tallinn",
'Africa/Cairo', "Africa/Cairo",
'Africa/El_Aaiun', "Africa/El_Aaiun",
'Europe/Madrid', "Europe/Madrid",
'Africa/Ceuta', "Africa/Ceuta",
'Atlantic/Canary', "Atlantic/Canary",
'Europe/Helsinki', "Europe/Helsinki",
'Pacific/Fiji', "Pacific/Fiji",
'Atlantic/Stanley', "Atlantic/Stanley",
'Pacific/Chuuk', "Pacific/Chuuk",
'Pacific/Pohnpei', "Pacific/Pohnpei",
'Pacific/Kosrae', "Pacific/Kosrae",
'Atlantic/Faroe', "Atlantic/Faroe",
'Europe/Paris', "Europe/Paris",
'Europe/London', "Europe/London",
'Asia/Tbilisi', "Asia/Tbilisi",
'America/Cayenne', "America/Cayenne",
'Africa/Accra', "Africa/Accra",
'Europe/Gibraltar', "Europe/Gibraltar",
'America/Godthab', "America/Godthab",
'America/Danmarkshavn', "America/Danmarkshavn",
'America/Scoresbysund', "America/Scoresbysund",
'America/Thule', "America/Thule",
'Europe/Athens', "Europe/Athens",
'Atlantic/South_Georgia', "Atlantic/South_Georgia",
'America/Guatemala', "America/Guatemala",
'Pacific/Guam', "Pacific/Guam",
'Africa/Bissau', "Africa/Bissau",
'America/Guyana', "America/Guyana",
'Asia/Hong_Kong', "Asia/Hong_Kong",
'America/Tegucigalpa', "America/Tegucigalpa",
'America/Port-au-Prince', "America/Port-au-Prince",
'Europe/Budapest', "Europe/Budapest",
'Asia/Jakarta', "Asia/Jakarta",
'Asia/Pontianak', "Asia/Pontianak",
'Asia/Makassar', "Asia/Makassar",
'Asia/Jayapura', "Asia/Jayapura",
'Europe/Dublin', "Europe/Dublin",
'Asia/Jerusalem', "Asia/Jerusalem",
'Asia/Kolkata', "Asia/Kolkata",
'Indian/Chagos', "Indian/Chagos",
'Asia/Baghdad', "Asia/Baghdad",
'Asia/Tehran', "Asia/Tehran",
'Atlantic/Reykjavik', "Atlantic/Reykjavik",
'Europe/Rome', "Europe/Rome",
'America/Jamaica', "America/Jamaica",
'Asia/Amman', "Asia/Amman",
'Asia/Tokyo', "Asia/Tokyo",
'Africa/Nairobi', "Africa/Nairobi",
'Asia/Bishkek', "Asia/Bishkek",
'Pacific/Tarawa', "Pacific/Tarawa",
'Pacific/Enderbury', "Pacific/Enderbury",
'Pacific/Kiritimati', "Pacific/Kiritimati",
'Asia/Pyongyang', "Asia/Pyongyang",
'Asia/Seoul', "Asia/Seoul",
'Asia/Almaty', "Asia/Almaty",
'Asia/Qyzylorda', "Asia/Qyzylorda",
'Asia/Aqtobe', "Asia/Aqtobe",
'Asia/Aqtau', "Asia/Aqtau",
'Asia/Atyrau', "Asia/Atyrau",
'Asia/Oral', "Asia/Oral",
'Asia/Beirut', "Asia/Beirut",
'Asia/Colombo', "Asia/Colombo",
'Africa/Monrovia', "Africa/Monrovia",
'Europe/Vilnius', "Europe/Vilnius",
'Europe/Luxembourg', "Europe/Luxembourg",
'Europe/Riga', "Europe/Riga",
'Africa/Tripoli', "Africa/Tripoli",
'Africa/Casablanca', "Africa/Casablanca",
'Europe/Monaco', "Europe/Monaco",
'Europe/Chisinau', "Europe/Chisinau",
'Pacific/Majuro', "Pacific/Majuro",
'Pacific/Kwajalein', "Pacific/Kwajalein",
'Asia/Yangon', "Asia/Yangon",
'Asia/Ulaanbaatar', "Asia/Ulaanbaatar",
'Asia/Hovd', "Asia/Hovd",
'Asia/Choibalsan', "Asia/Choibalsan",
'Asia/Macau', "Asia/Macau",
'America/Martinique', "America/Martinique",
'Europe/Malta', "Europe/Malta",
'Indian/Mauritius', "Indian/Mauritius",
'Indian/Maldives', "Indian/Maldives",
'America/Mexico_City', "America/Mexico_City",
'America/Cancun', "America/Cancun",
'America/Merida', "America/Merida",
'America/Monterrey', "America/Monterrey",
'America/Matamoros', "America/Matamoros",
'America/Mazatlan', "America/Mazatlan",
'America/Chihuahua', "America/Chihuahua",
'America/Ojinaga', "America/Ojinaga",
'America/Hermosillo', "America/Hermosillo",
'America/Tijuana', "America/Tijuana",
'America/Bahia_Banderas', "America/Bahia_Banderas",
'Asia/Kuala_Lumpur', "Asia/Kuala_Lumpur",
'Asia/Kuching', "Asia/Kuching",
'Africa/Maputo', "Africa/Maputo",
'Africa/Windhoek', "Africa/Windhoek",
'Pacific/Noumea', "Pacific/Noumea",
'Pacific/Norfolk', "Pacific/Norfolk",
'Africa/Lagos', "Africa/Lagos",
'America/Managua', "America/Managua",
'Europe/Amsterdam', "Europe/Amsterdam",
'Europe/Oslo', "Europe/Oslo",
'Asia/Kathmandu', "Asia/Kathmandu",
'Pacific/Nauru', "Pacific/Nauru",
'Pacific/Niue', "Pacific/Niue",
'Pacific/Auckland', "Pacific/Auckland",
'Pacific/Chatham', "Pacific/Chatham",
'America/Panama', "America/Panama",
'America/Lima', "America/Lima",
'Pacific/Tahiti', "Pacific/Tahiti",
'Pacific/Marquesas', "Pacific/Marquesas",
'Pacific/Gambier', "Pacific/Gambier",
'Pacific/Port_Moresby', "Pacific/Port_Moresby",
'Pacific/Bougainville', "Pacific/Bougainville",
'Asia/Manila', "Asia/Manila",
'Asia/Karachi', "Asia/Karachi",
'Europe/Warsaw', "Europe/Warsaw",
'America/Miquelon', "America/Miquelon",
'Pacific/Pitcairn', "Pacific/Pitcairn",
'America/Puerto_Rico', "America/Puerto_Rico",
'Asia/Gaza', "Asia/Gaza",
'Asia/Hebron', "Asia/Hebron",
'Europe/Lisbon', "Europe/Lisbon",
'Atlantic/Madeira', "Atlantic/Madeira",
'Atlantic/Azores', "Atlantic/Azores",
'Pacific/Palau', "Pacific/Palau",
'America/Asuncion', "America/Asuncion",
'Asia/Qatar', "Asia/Qatar",
'Indian/Reunion', "Indian/Reunion",
'Europe/Bucharest', "Europe/Bucharest",
'Europe/Belgrade', "Europe/Belgrade",
'Europe/Kaliningrad', "Europe/Kaliningrad",
'Europe/Moscow', "Europe/Moscow",
'Europe/Simferopol', "Europe/Simferopol",
'Europe/Kirov', "Europe/Kirov",
'Europe/Astrakhan', "Europe/Astrakhan",
'Europe/Volgograd', "Europe/Volgograd",
'Europe/Saratov', "Europe/Saratov",
'Europe/Ulyanovsk', "Europe/Ulyanovsk",
'Europe/Samara', "Europe/Samara",
'Asia/Yekaterinburg', "Asia/Yekaterinburg",
'Asia/Omsk', "Asia/Omsk",
'Asia/Novosibirsk', "Asia/Novosibirsk",
'Asia/Barnaul', "Asia/Barnaul",
'Asia/Tomsk', "Asia/Tomsk",
'Asia/Novokuznetsk', "Asia/Novokuznetsk",
'Asia/Krasnoyarsk', "Asia/Krasnoyarsk",
'Asia/Irkutsk', "Asia/Irkutsk",
'Asia/Chita', "Asia/Chita",
'Asia/Yakutsk', "Asia/Yakutsk",
'Asia/Khandyga', "Asia/Khandyga",
'Asia/Vladivostok', "Asia/Vladivostok",
'Asia/Ust-Nera', "Asia/Ust-Nera",
'Asia/Magadan', "Asia/Magadan",
'Asia/Sakhalin', "Asia/Sakhalin",
'Asia/Srednekolymsk', "Asia/Srednekolymsk",
'Asia/Kamchatka', "Asia/Kamchatka",
'Asia/Anadyr', "Asia/Anadyr",
'Asia/Riyadh', "Asia/Riyadh",
'Pacific/Guadalcanal', "Pacific/Guadalcanal",
'Indian/Mahe', "Indian/Mahe",
'Africa/Khartoum', "Africa/Khartoum",
'Europe/Stockholm', "Europe/Stockholm",
'Asia/Singapore', "Asia/Singapore",
'America/Paramaribo', "America/Paramaribo",
'Africa/Juba', "Africa/Juba",
'Africa/Sao_Tome', "Africa/Sao_Tome",
'America/El_Salvador', "America/El_Salvador",
'Asia/Damascus', "Asia/Damascus",
'America/Grand_Turk', "America/Grand_Turk",
'Africa/Ndjamena', "Africa/Ndjamena",
'Indian/Kerguelen', "Indian/Kerguelen",
'Asia/Bangkok', "Asia/Bangkok",
'Asia/Dushanbe', "Asia/Dushanbe",
'Pacific/Fakaofo', "Pacific/Fakaofo",
'Asia/Dili', "Asia/Dili",
'Asia/Ashgabat', "Asia/Ashgabat",
'Africa/Tunis', "Africa/Tunis",
'Pacific/Tongatapu', "Pacific/Tongatapu",
'Europe/Istanbul', "Europe/Istanbul",
'America/Port_of_Spain', "America/Port_of_Spain",
'Pacific/Funafuti', "Pacific/Funafuti",
'Asia/Taipei', "Asia/Taipei",
'Europe/Kiev', "Europe/Kiev",
'Europe/Uzhgorod', "Europe/Uzhgorod",
'Europe/Zaporozhye', "Europe/Zaporozhye",
'Pacific/Wake', "Pacific/Wake",
'America/New_York', "America/New_York",
'America/Detroit', "America/Detroit",
'America/Kentucky/Louisville', "America/Kentucky/Louisville",
'America/Kentucky/Monticello', "America/Kentucky/Monticello",
'America/Indiana/Indianapolis', "America/Indiana/Indianapolis",
'America/Indiana/Vincennes', "America/Indiana/Vincennes",
'America/Indiana/Winamac', "America/Indiana/Winamac",
'America/Indiana/Marengo', "America/Indiana/Marengo",
'America/Indiana/Petersburg', "America/Indiana/Petersburg",
'America/Indiana/Vevay', "America/Indiana/Vevay",
'America/Chicago', "America/Chicago",
'America/Indiana/Tell_City', "America/Indiana/Tell_City",
'America/Indiana/Knox', "America/Indiana/Knox",
'America/Menominee', "America/Menominee",
'America/North_Dakota/Center', "America/North_Dakota/Center",
'America/North_Dakota/New_Salem', "America/North_Dakota/New_Salem",
'America/North_Dakota/Beulah', "America/North_Dakota/Beulah",
'America/Denver', "America/Denver",
'America/Boise', "America/Boise",
'America/Phoenix', "America/Phoenix",
'America/Los_Angeles', "America/Los_Angeles",
'America/Anchorage', "America/Anchorage",
'America/Juneau', "America/Juneau",
'America/Sitka', "America/Sitka",
'America/Metlakatla', "America/Metlakatla",
'America/Yakutat', "America/Yakutat",
'America/Nome', "America/Nome",
'America/Adak', "America/Adak",
'Pacific/Honolulu', "Pacific/Honolulu",
'America/Montevideo', "America/Montevideo",
'Asia/Samarkand', "Asia/Samarkand",
'Asia/Tashkent', "Asia/Tashkent",
'America/Caracas', "America/Caracas",
'Asia/Ho_Chi_Minh', "Asia/Ho_Chi_Minh",
'Pacific/Efate', "Pacific/Efate",
'Pacific/Wallis', "Pacific/Wallis",
'Pacific/Apia', "Pacific/Apia",
'Africa/Johannesburg', "Africa/Johannesburg",
]; ];
export function timezoneList() { export function timezoneList() {
let result = []; let result = [];
@ -404,12 +404,14 @@ export function timezoneList() {
result.sort((a, b) => { result.sort((a, b) => {
if (a.time > b.time) { if (a.time > b.time) {
return 1; return 1;
} else if (b.time > a.time) {
return -1;
} else {
return 0;
} }
if (b.time > a.time) {
return -1;
}
return 0;
}) })
return result; return result;
}; }