diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index 0ee5943a7..bf76d9eb6 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -15,14 +15,14 @@ on: jobs: auto-test: - needs: [ check-linters, e2e-test ] + needs: [ check-linters ] runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest, ARM64] - node: [ 18, 20.5 ] + node: [ 18, 20 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -42,7 +42,7 @@ jobs: # As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works armv7-simple-test: - needs: [ check-linters ] + needs: [ ] runs-on: ${{ matrix.os }} timeout-minutes: 15 if: ${{ github.repository == 'louislam/uptime-kuma' }} @@ -77,7 +77,7 @@ jobs: - run: npm run lint:prod e2e-test: - needs: [ check-linters ] + needs: [ ] runs-on: ARM64 steps: - run: git config --global core.autocrlf false # Mainly for Windows diff --git a/extra/download-dist.js b/extra/download-dist.js index b8be5eb8a..b339ac930 100644 --- a/extra/download-dist.js +++ b/extra/download-dist.js @@ -4,7 +4,6 @@ const tar = require("tar"); const packageJSON = require("../package.json"); const fs = require("fs"); -const rmSync = require("./fs-rmSync.js"); const version = packageJSON.version; const filename = "dist.tar.gz"; @@ -29,8 +28,9 @@ function download(url) { if (fs.existsSync("./dist")) { if (fs.existsSync("./dist-backup")) { - rmSync("./dist-backup", { - recursive: true + fs.rmSync("./dist-backup", { + recursive: true, + force: true, }); } @@ -43,8 +43,9 @@ function download(url) { tarStream.on("close", () => { if (fs.existsSync("./dist-backup")) { - rmSync("./dist-backup", { - recursive: true + fs.rmSync("./dist-backup", { + recursive: true, + force: true, }); } console.log("Done"); diff --git a/extra/fs-rmSync.js b/extra/fs-rmSync.js deleted file mode 100644 index a42e30a68..000000000 --- a/extra/fs-rmSync.js +++ /dev/null @@ -1,23 +0,0 @@ -const fs = require("fs"); -/** - * Detect if `fs.rmSync` is available - * to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16, - * or the `recursive` property removing completely in the future Node.js version. - * See the link below. - * @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`. - * @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync` - * @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync` - * @param {fs.PathLike} path Valid types for path values in "fs". - * @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`. - * @returns {void} - */ -const rmSync = (path, options) => { - if (typeof fs.rmSync === "function") { - if (options.recursive) { - options.force = true; - } - return fs.rmSync(path, options); - } - return fs.rmdirSync(path, options); -}; -module.exports = rmSync; diff --git a/extra/update-language-files/index.js b/extra/update-language-files/index.js index 5b748a98e..acb6bd467 100644 --- a/extra/update-language-files/index.js +++ b/extra/update-language-files/index.js @@ -2,7 +2,6 @@ import fs from "fs"; import util from "util"; -import rmSync from "../fs-rmSync.js"; /** * Copy across the required language files @@ -16,7 +15,10 @@ import rmSync from "../fs-rmSync.js"; */ function copyFiles(langCode, baseLang) { if (fs.existsSync("./languages")) { - rmSync("./languages", { recursive: true }); + fs.rmSync("./languages", { + recursive: true, + force: true, + }); } fs.mkdirSync("./languages"); @@ -93,6 +95,9 @@ console.log("Updating: " + langCode); copyFiles(langCode, baseLangCode); await updateLanguage(langCode, baseLangCode); -rmSync("./languages", { recursive: true }); +fs.rmSync("./languages", { + recursive: true, + force: true, +}); console.log("Done. Fixing formatting by ESLint..."); diff --git a/package.json b/package.json index cf6348cbd..0d51984c7 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,7 @@ "sort-contributors": "node extra/sort-contributors.js", "quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2", "start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate", - "rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X", - "start-server-node14-win": "private\\node14\\node.exe server/server.js" + "rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X" }, "dependencies": { "@grpc/grpc-js": "~1.8.22", diff --git a/server/model/monitor.js b/server/model/monitor.js index 4beeb0036..78485c4c5 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -72,23 +72,12 @@ class Monitor extends BeanModel { /** * Return an object that ready to parse to JSON + * @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function * @param {boolean} includeSensitiveData Include sensitive data in * JSON - * @returns {Promise} Object ready to parse + * @returns {object} Object ready to parse */ - async toJSON(includeSensitiveData = true) { - - let notificationIDList = {}; - - let list = await R.find("monitor_notification", " monitor_id = ? ", [ - this.id, - ]); - - for (let bean of list) { - notificationIDList[bean.notification_id] = true; - } - - const tags = await this.getTags(); + toJSON(preloadData = {}, includeSensitiveData = true) { let screenshot = null; @@ -96,7 +85,7 @@ class Monitor extends BeanModel { screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png"; } - const path = await this.getPath(); + const path = preloadData.paths.get(this.id) || []; const pathName = path.join(" / "); let data = { @@ -106,15 +95,15 @@ class Monitor extends BeanModel { path, pathName, parent: this.parent, - childrenIDs: await Monitor.getAllChildrenIDs(this.id), + childrenIDs: preloadData.childrenIDs.get(this.id) || [], url: this.url, method: this.method, hostname: this.hostname, port: this.port, maxretries: this.maxretries, weight: this.weight, - active: await this.isActive(), - forceInactive: !await Monitor.isParentActive(this.id), + active: preloadData.activeStatus.get(this.id), + forceInactive: preloadData.forceInactive.get(this.id), type: this.type, timeout: this.timeout, interval: this.interval, @@ -134,9 +123,9 @@ class Monitor extends BeanModel { docker_container: this.docker_container, docker_host: this.docker_host, proxyId: this.proxy_id, - notificationIDList, - tags: tags, - maintenance: await Monitor.isUnderMaintenance(this.id), + notificationIDList: preloadData.notifications.get(this.id) || {}, + tags: preloadData.tags.get(this.id) || [], + maintenance: preloadData.maintenanceStatus.get(this.id), mqttTopic: this.mqttTopic, mqttSuccessMessage: this.mqttSuccessMessage, mqttCheckType: this.mqttCheckType, @@ -202,16 +191,6 @@ class Monitor extends BeanModel { return data; } - /** - * Checks if the monitor is active based on itself and its parents - * @returns {Promise} Is the monitor active? - */ - async isActive() { - const parentActive = await Monitor.isParentActive(this.id); - - return (this.active === 1) && parentActive; - } - /** * Get all tags applied to this monitor * @returns {Promise[]>} List of tags on the @@ -1197,6 +1176,18 @@ class Monitor extends BeanModel { return checkCertificateResult; } + /** + * Checks if the monitor is active based on itself and its parents + * @param {number} monitorID ID of monitor to send + * @param {boolean} active is active + * @returns {Promise} Is the monitor active? + */ + static async isActive(monitorID, active) { + const parentActive = await Monitor.isParentActive(monitorID); + + return (active === 1) && parentActive; + } + /** * Send statistics to clients * @param {Server} io Socket server instance @@ -1333,7 +1324,10 @@ class Monitor extends BeanModel { for (let notification of notificationList) { try { const heartbeatJSON = bean.toJSON(); - + const monitorData = [{ id: monitor.id, + active: monitor.active + }]; + const preloadData = await Monitor.preparePreloadData(monitorData); // Prevent if the msg is undefined, notifications such as Discord cannot send out. if (!heartbeatJSON["msg"]) { heartbeatJSON["msg"] = "N/A"; @@ -1344,7 +1338,7 @@ class Monitor extends BeanModel { heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset(); heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT); - await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON); + await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON); } catch (e) { log.error("monitor", "Cannot send notification to " + notification.name); log.error("monitor", e); @@ -1507,6 +1501,111 @@ class Monitor extends BeanModel { } } + /** + * Gets monitor notification of multiple monitor + * @param {Array} monitorIDs IDs of monitor to get + * @returns {Promise>} object + */ + static async getMonitorNotification(monitorIDs) { + return await R.getAll(` + SELECT monitor_notification.monitor_id, monitor_notification.notification_id + FROM monitor_notification + WHERE monitor_notification.monitor_id IN (?) + `, [ + monitorIDs, + ]); + } + + /** + * Gets monitor tags of multiple monitor + * @param {Array} monitorIDs IDs of monitor to get + * @returns {Promise>} object + */ + static async getMonitorTag(monitorIDs) { + return await R.getAll(` + SELECT monitor_tag.monitor_id, tag.name, tag.color + FROM monitor_tag + JOIN tag ON monitor_tag.tag_id = tag.id + WHERE monitor_tag.monitor_id IN (?) + `, [ + monitorIDs, + ]); + } + + /** + * prepare preloaded data for efficient access + * @param {Array} monitorData IDs & active field of monitor to get + * @returns {Promise>} object + */ + static async preparePreloadData(monitorData) { + + const notificationsMap = new Map(); + const tagsMap = new Map(); + const maintenanceStatusMap = new Map(); + const childrenIDsMap = new Map(); + const activeStatusMap = new Map(); + const forceInactiveMap = new Map(); + const pathsMap = new Map(); + + if (monitorData.length > 0) { + const monitorIDs = monitorData.map(monitor => monitor.id); + const notifications = await Monitor.getMonitorNotification(monitorIDs); + const tags = await Monitor.getMonitorTag(monitorIDs); + const maintenanceStatuses = await Promise.all(monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id))); + const childrenIDs = await Promise.all(monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id))); + const activeStatuses = await Promise.all(monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active))); + const forceInactiveStatuses = await Promise.all(monitorData.map(monitor => Monitor.isParentActive(monitor.id))); + const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name))); + + notifications.forEach(row => { + if (!notificationsMap.has(row.monitor_id)) { + notificationsMap.set(row.monitor_id, {}); + } + notificationsMap.get(row.monitor_id)[row.notification_id] = true; + }); + + tags.forEach(row => { + if (!tagsMap.has(row.monitor_id)) { + tagsMap.set(row.monitor_id, []); + } + tagsMap.get(row.monitor_id).push({ + name: row.name, + color: row.color + }); + }); + + monitorData.forEach((monitor, index) => { + maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]); + }); + + monitorData.forEach((monitor, index) => { + childrenIDsMap.set(monitor.id, childrenIDs[index]); + }); + + monitorData.forEach((monitor, index) => { + activeStatusMap.set(monitor.id, activeStatuses[index]); + }); + + monitorData.forEach((monitor, index) => { + forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]); + }); + + monitorData.forEach((monitor, index) => { + pathsMap.set(monitor.id, paths[index]); + }); + } + + return { + notifications: notificationsMap, + tags: tagsMap, + maintenanceStatus: maintenanceStatusMap, + childrenIDs: childrenIDsMap, + activeStatus: activeStatusMap, + forceInactive: forceInactiveMap, + paths: pathsMap, + }; + } + /** * Gets Parent of the monitor * @param {number} monitorID ID of monitor to get @@ -1539,16 +1638,18 @@ class Monitor extends BeanModel { /** * Gets the full path + * @param {number} monitorID ID of the monitor to get + * @param {string} name of the monitor to get * @returns {Promise} Full path (includes groups and the name) of the monitor */ - async getPath() { - const path = [ this.name ]; + static async getAllPath(monitorID, name) { + const path = [ name ]; if (this.parent === null) { return path; } - let parent = await Monitor.getParent(this.id); + let parent = await Monitor.getParent(monitorID); while (parent !== null) { path.unshift(parent.name); parent = await Monitor.getParent(parent.id); diff --git a/server/notification-providers/serverchan.js b/server/notification-providers/serverchan.js index cefe61f14..ab9308c3e 100644 --- a/server/notification-providers/serverchan.js +++ b/server/notification-providers/serverchan.js @@ -11,8 +11,13 @@ class ServerChan extends NotificationProvider { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { const okMsg = "Sent Successfully."; + // serverchan3 requires sending via ft07.com + const url = String(notification.serverChanSendKey).startsWith("sctp") + ? `https://${notification.serverChanSendKey}.push.ft07.com/send` + : `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`; + try { - await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, { + await axios.post(url, { "title": this.checkStatus(heartbeatJSON, monitorJSON), "desp": msg, }); diff --git a/server/server.js b/server/server.js index d040d6e87..3579df5d2 100644 --- a/server/server.js +++ b/server/server.js @@ -726,7 +726,7 @@ let needSetup = false; await updateMonitorNotification(bean.id, notificationIDList); - await server.sendMonitorList(socket); + await server.sendUpdateMonitorIntoList(socket, bean.id); if (monitor.active !== false) { await startMonitor(socket.userID, bean.id); @@ -879,11 +879,11 @@ let needSetup = false; await updateMonitorNotification(bean.id, monitor.notificationIDList); - if (await bean.isActive()) { + if (await Monitor.isActive(bean.id, bean.active)) { await restartMonitor(socket.userID, bean.id); } - await server.sendMonitorList(socket); + await server.sendUpdateMonitorIntoList(socket, bean.id); callback({ ok: true, @@ -923,14 +923,17 @@ let needSetup = false; log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`); - let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ + let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ monitorID, socket.userID, ]); - + const monitorData = [{ id: monitor.id, + active: monitor.active + }]; + const preloadData = await Monitor.preparePreloadData(monitorData); callback({ ok: true, - monitor: await bean.toJSON(), + monitor: monitor.toJSON(preloadData), }); } catch (e) { @@ -981,7 +984,7 @@ let needSetup = false; try { checkLogin(socket); await startMonitor(socket.userID, monitorID); - await server.sendMonitorList(socket); + await server.sendUpdateMonitorIntoList(socket, monitorID); callback({ ok: true, @@ -1001,7 +1004,7 @@ let needSetup = false; try { checkLogin(socket); await pauseMonitor(socket.userID, monitorID); - await server.sendMonitorList(socket); + await server.sendUpdateMonitorIntoList(socket, monitorID); callback({ ok: true, @@ -1047,8 +1050,7 @@ let needSetup = false; msg: "successDeleted", msgi18n: true, }); - - await server.sendMonitorList(socket); + await server.sendDeleteMonitorFromList(socket, monitorID); } catch (e) { callback({ @@ -1678,13 +1680,13 @@ async function afterLogin(socket, user) { await StatusPage.sendStatusPageList(io, socket); + const monitorPromises = []; for (let monitorID in monitorList) { - await sendHeartbeatList(socket, monitorID); + monitorPromises.push(sendHeartbeatList(socket, monitorID)); + monitorPromises.push(Monitor.sendStats(io, monitorID, user.id)); } - for (let monitorID in monitorList) { - await Monitor.sendStats(io, monitorID, user.id); - } + await Promise.all(monitorPromises); // Set server timezone from client browser if not set // It should be run once only diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 573d791a6..76bf42565 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -205,24 +205,56 @@ class UptimeKumaServer { return list; } + /** + * Update Monitor into list + * @param {Socket} socket Socket to send list on + * @param {number} monitorID update or deleted monitor id + * @returns {Promise} + */ + async sendUpdateMonitorIntoList(socket, monitorID) { + let list = await this.getMonitorJSONList(socket.userID, monitorID); + this.io.to(socket.userID).emit("updateMonitorIntoList", list); + } + + /** + * Delete Monitor from list + * @param {Socket} socket Socket to send list on + * @param {number} monitorID update or deleted monitor id + * @returns {Promise} + */ + async sendDeleteMonitorFromList(socket, monitorID) { + this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID); + } + /** * Get a list of monitors for the given user. * @param {string} userID - The ID of the user to get monitors for. + * @param {number} monitorID - The ID of monitor for. * @returns {Promise} A promise that resolves to an object with monitor IDs as keys and monitor objects as values. * * Generated by Trelent */ - async getMonitorJSONList(userID) { - let result = {}; + async getMonitorJSONList(userID, monitorID = null) { - let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [ - userID, - ]); + let query = " user_id = ? "; + let queryParams = [ userID ]; - for (let monitor of monitorList) { - result[monitor.id] = await monitor.toJSON(); + if (monitorID) { + query += "AND id = ? "; + queryParams.push(monitorID); } + let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams); + + const monitorData = monitorList.map(monitor => ({ + id: monitor.id, + active: monitor.active, + name: monitor.name, + })); + const preloadData = await Monitor.preparePreloadData(monitorData); + + const result = {}; + monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData)); return result; } @@ -520,3 +552,4 @@ const { DnsMonitorType } = require("./monitor-types/dns"); const { MqttMonitorType } = require("./monitor-types/mqtt"); const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); +const Monitor = require("./model/monitor"); diff --git a/src/components/settings/MonitorHistory.vue b/src/components/settings/MonitorHistory.vue index befedf513..25e3e1559 100644 --- a/src/components/settings/MonitorHistory.vue +++ b/src/components/settings/MonitorHistory.vue @@ -32,7 +32,14 @@ -
{{ $t("shrinkDatabaseDescription") }}
+ + + + + @@ -1000,9 +1010,58 @@ +