diff --git a/package-lock.json b/package-lock.json index b8a7b92a4..cdae49c8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14,6 +14,12 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" }, + "@babel/standalone": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.14.7.tgz", + "integrity": "sha512-7RlfMPR4604SbYpj5zvs2ZK587hVhixgU9Pd9Vs8lB8KYtT3U0apXSf0vZXhy8XRh549eUmJOHXhWKTO3ObzOQ==", + "dev": true + }, "@babel/types": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", @@ -48,6 +54,19 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", "integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" }, + "@vitejs/plugin-legacy": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.3.tgz", + "integrity": "sha512-lxZUJaMWYMQuqvZM1wPzDP6KABQgA/drVL5fnaygEPcz9adc2OHhfFNN/SvvHQ1V0rP8gybIc7uA+iI1gAdkVQ==", + "dev": true, + "requires": { + "@babel/standalone": "^7.14.7", + "core-js": "^3.15.1", + "magic-string": "^0.25.7", + "regenerator-runtime": "^0.13.7", + "systemjs": "^6.10.1" + } + }, "@vitejs/plugin-vue": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.3.tgz", @@ -618,6 +637,12 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "core-js": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz", + "integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -2682,6 +2707,12 @@ } } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -3203,6 +3234,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "systemjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.10.2.tgz", + "integrity": "sha512-PwaC0Z6Y1E6gFekY2u38EC5+5w2M65jYVrD1aAcOptpHVhCwPIwPFJvYJyryQKUyeuQ5bKKI3PBHWNjdE9aizg==", + "dev": true + }, "tarn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", diff --git a/package.json b/package.json index 44de9d8d3..359b2fe25 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "1.0.0", "scripts": { "dev": "vite --host", - "dev-server": "node server/server.js", - "build": "vite build", - "serve": "vite preview --host" + "start-server": "node server/server.js", + "update": "", + "build": "npm install && vite build", + "vite-preview-dist": "vite preview --host" }, "dependencies": { "@popperjs/core": "^2.9.2", @@ -25,8 +26,10 @@ "vue-toastification": "^2.0.0-rc.1" }, "devDependencies": { + "@vitejs/plugin-legacy": "^1.4.3", "@vitejs/plugin-vue": "^1.2.3", "@vue/compiler-sfc": "^3.0.5", + "core-js": "^3.15.2", "sass": "^1.35.1", "vite": "^2.3.7" } diff --git a/server/model/monitor.js b/server/model/monitor.js index 8655e4694..062f34061 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -141,71 +141,52 @@ class Monitor extends BeanModel { } /** - * + * Uptime with calculation + * Calculation based on: + * https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime * @param duration : int Hours */ static async sendUptime(duration, io, monitorID, userID) { let sec = duration * 3600; let downtimeList = await R.getAll(` - SELECT duration, time + SELECT duration, time, status FROM heartbeat WHERE time > DATE('now', ? || ' hours') - AND status = 0 AND monitor_id = ? `, [ -duration, monitorID ]); let downtime = 0; - let uptime = 0; + let total = 0; + let uptime; - if (downtimeList.length === 0) { - for (let row of downtimeList) { - let value = parseInt(row.duration) - let time = row.time + for (let row of downtimeList) { + let value = parseInt(row.duration) + let time = row.time - // Handle if heartbeat duration longer than the target duration - // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs - if (value <= sec) { - downtime += value; - } else { - let trim = dayjs.utc().diff(dayjs(time), 'second'); + // Handle if heartbeat duration longer than the target duration + // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs + if (value > sec) { + let trim = dayjs.utc().diff(dayjs(time), 'second'); + value = sec - trim; - value = sec - trim; - - if (value < 0) { - value = 0; - } - downtime += value; + if (value < 0) { + value = 0; } } - uptime = (sec - downtime) / sec; - - if (uptime < 0) { - uptime = 0; + total += value; + if (row.status === 0) { + downtime += value; } - } else { - // This case for someone who are not running UptimeKuma 24x7. - // If there is no heartbeat in this time range, use last heartbeat as reference - // If is down, uptime = 0 - // If is up, uptime = 1 + } - let lastHeartbeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ - monitorID - ]); + uptime = (total - downtime) / total; - if (lastHeartbeat) { - if (lastHeartbeat.status === 1) { - uptime = 1; - } else { - uptime = 0; - } - } else { - // No heartbeat is found, assume 100% - uptime = 1; - } + if (uptime < 0) { + uptime = 0; } io.to(userID).emit("uptime", monitorID, duration, uptime); diff --git a/server/notification.js b/server/notification.js new file mode 100644 index 000000000..de9c30b9f --- /dev/null +++ b/server/notification.js @@ -0,0 +1,58 @@ +const axios = require("axios"); +const {R} = require("redbean-node"); + +class Notification { + static async send(notification, msg) { + if (notification.type === "telegram") { + let res = await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { + params: { + chat_id: notification.telegramChatID, + text: msg, + } + }) + return true; + } else { + throw new Error("Notification type is not supported") + } + } + + static async save(notification, notificationID, userID) { + let bean + + if (notificationID) { + bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ + notificationID, + userID, + ]) + + if (! bean) { + throw new Error("notification not found") + } + + } else { + bean = R.dispense("notification") + } + + bean.name = notification.name; + bean.user_id = userID; + bean.config = JSON.stringify(notification) + await R.store(bean) + } + + static async delete(notificationID, userID) { + let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [ + notificationID, + userID, + ]) + + if (! bean) { + throw new Error("notification not found") + } + + await R.trash(bean) + } +} + +module.exports = { + Notification, +} diff --git a/server/server.js b/server/server.js index 3f9daf2e6..bf30ec814 100644 --- a/server/server.js +++ b/server/server.js @@ -10,6 +10,7 @@ const passwordHash = require('password-hash'); const jwt = require('jsonwebtoken'); const Monitor = require("./model/monitor"); const {getSettings} = require("./util-server"); +const {Notification} = require("./notification") let totalClient = 0; let jwtSecret = null; @@ -26,6 +27,10 @@ let monitorList = {}; app.use('/', express.static("dist")); + app.get('*', function(request, response, next) { + response.sendFile(process.cwd() + '/dist/index.html'); + }); + io.on('connection', async (socket) => { console.log('a user connected'); totalClient++; @@ -318,6 +323,65 @@ let monitorList = {}; } }); + // Add or Edit + socket.on("addNotification", async (notification, notificationID, callback) => { + try { + checkLogin(socket) + + await Notification.save(notification, notificationID, socket.userID) + await sendNotificationList(socket) + + callback({ + ok: true, + msg: "Saved", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message + }); + } + }); + + socket.on("deleteNotification", async (notificationID, callback) => { + try { + checkLogin(socket) + + await Notification.delete(notificationID, socket.userID) + await sendNotificationList(socket) + + callback({ + ok: true, + msg: "Deleted", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message + }); + } + }); + + socket.on("testNotification", async (notification, callback) => { + try { + checkLogin(socket) + + await Notification.send(notification, notification.name + " Testing") + + callback({ + ok: true, + msg: "Sent Successfully" + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message + }); + } + }); }); server.listen(3001, () => { @@ -344,6 +408,20 @@ async function sendMonitorList(socket) { return list; } +async function sendNotificationList(socket) { + let result = []; + let list = await R.find("notification", " user_id = ? ", [ + socket.userID + ]); + + for (let bean of list) { + result.push(bean.export()) + } + + io.to(socket.userID).emit("notificationList", result) + return list; +} + async function afterLogin(socket, user) { socket.userID = user.id; socket.join(user.id) @@ -355,6 +433,8 @@ async function afterLogin(socket, user) { await sendImportantHeartbeatList(socket, monitorID); await Monitor.sendStats(io, monitorID, user.id) } + + await sendNotificationList(socket) } async function getMonitorJSONList(userID) { diff --git a/server/util-server.js b/server/util-server.js index ca8e9f8ee..6fdef9422 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,5 +1,6 @@ const tcpp = require('tcp-ping'); const Ping = require("./ping-lite"); +const {R} = require("redbean-node"); exports.tcping = function (hostname, port) { return new Promise((resolve, reject) => { @@ -37,3 +38,25 @@ exports.ping = function (hostname) { }); }); } + +exports.setting = async function (key) { + return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ + key + ]) +} + +exports.getSettings = async function (type) { + let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [ + type + ]) + + let result = {}; + + for (let row of list) { + result[row.key] = row.value; + } + + console.log(result) + + return result; +} diff --git a/server/util.js b/server/util.js index c6f61dfad..dfe4eaa06 100644 --- a/server/util.js +++ b/server/util.js @@ -10,6 +10,10 @@ export function sleep(ms) { } export function ucfirst(str) { + if (! str) { + return str; + } + const firstLetter = str.substr(0, 1); return firstLetter.toUpperCase() + str.substr(1); } diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 6df3c3f56..1078195c6 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -1,42 +1,81 @@