diff --git a/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js new file mode 100644 index 000000000..16d307eb5 --- /dev/null +++ b/db/knex_migrations/2025-06-11-0000-add-manual-monitor.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.string("manual_status").defaultTo(null); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("manual_status"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index 8d70aa54b..741fb940e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -636,17 +636,7 @@ class Monitor extends BeanModel { bean.duration = beatInterval; throw new Error("No heartbeat in the time window"); } - } else if (this.type === "manual") { - const lastHeartbeat = await Monitor.getPreviousHeartbeat(this.id); - if (lastHeartbeat) { - bean.status = lastHeartbeat.status; - bean.msg = lastHeartbeat.msg || "Manual monitoring"; - } else { - bean.status = PENDING; - bean.msg = "Manual monitoring - No previous status"; - } - bean.time = new Date().getTime(); - retries = 0; + } else if (this.type === "steam") { const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; const steamAPIKey = await setting("steamAPIKey"); diff --git a/server/monitor-types/manual.js b/server/monitor-types/manual.js index 1ae2cda6a..53b3366d5 100644 --- a/server/monitor-types/manual.js +++ b/server/monitor-types/manual.js @@ -1,23 +1,33 @@ const { MonitorType } = require("./monitor-type"); +const { UP, DOWN, PENDING, MAINTENANCE } = require("../../src/util"); class ManualMonitorType extends MonitorType { name = "Manual"; type = "manual"; - description = "Manual monitoring"; + description = "A monitor that allows manual control of the status"; supportsConditions = false; conditionVariables = []; /** - * Checks the status of the monitor manually - * @param {object} monitor - Monitor object + * Checks the status of the monitor based on the manually set status + * This monitor type is specifically designed for status pages where manual control is needed + * @param {object} monitor - Monitor object containing the current status and message * @param {object} heartbeat - Object to write the status of the check - * @param {object} server - Server object * @returns {Promise} */ - async check(monitor, heartbeat, server) { - heartbeat.status = monitor.status; - heartbeat.msg = monitor.msg || "Manual monitoring"; - heartbeat.time = new Date().getTime(); + async check(monitor, heartbeat) { + if (monitor.manual_status !== null) { + heartbeat.status = monitor.manual_status; + heartbeat.msg = monitor.manual_status === UP ? "Up" : + monitor.manual_status === DOWN ? "Down" : + monitor.manual_status === MAINTENANCE ? "Maintenance" : + "Pending"; + heartbeat.ping = null; + } else { + heartbeat.status = PENDING; + heartbeat.msg = "Manual monitoring - No status set"; + heartbeat.ping = null; + } } } diff --git a/server/server.js b/server/server.js index 0a418595b..5fd2371f3 100644 --- a/server/server.js +++ b/server/server.js @@ -37,7 +37,7 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) { } const args = require("args-parser")(process.argv); -const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util"); +const { sleep, log, getRandomInt, genSecret, isDev, UP, DOWN, PENDING, MAINTENANCE } = require("../src/util"); const config = require("./config"); log.debug("server", "Arguments"); @@ -1542,34 +1542,6 @@ let needSetup = false; } }); - socket.on("addHeartbeat", async (heartbeat, callback) => { - try { - checkLogin(socket); - - let bean = R.dispense("heartbeat"); - bean.monitor_id = heartbeat.monitorID; - bean.status = heartbeat.status; - bean.msg = heartbeat.msg; - bean.time = heartbeat.time; - bean.ping = heartbeat.ping; - bean.important = true; - - await R.store(bean); - - io.to(socket.userID).emit("heartbeat", bean.toJSON()); - - callback({ - ok: true, - }); - - } catch (e) { - callback({ - ok: false, - msg: e.message, - }); - } - }); - socket.on("clearStatistics", async (callback) => { try { checkLogin(socket); @@ -1626,6 +1598,48 @@ let needSetup = false; log.debug("auth", "need auth"); } + socket.on("updateManual", async (data, callback) => { + try { + checkLogin(socket); + + let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ + data.monitorID, + socket.userID, + ]); + + if (!monitor) { + throw new Error("Monitor not found"); + } + + let status; + if (data.status === 1) { + status = UP; + } else if (data.status === 0) { + status = DOWN; + } else if (data.status === 3) { + status = MAINTENANCE; + } else { + status = PENDING; + } + + monitor.manual_status = status; + await R.store(monitor); + + callback({ + ok: true, + msg: "Saved.", + msgi18n: true, + id: monitor.id, + }); + + } catch (e) { + callback({ + ok: true, + msg: e.message, + }); + } + }); + }); log.debug("server", "Init the server"); diff --git a/server/uptime-calculator.js b/server/uptime-calculator.js index bdc487f07..71d1d458c 100644 --- a/server/uptime-calculator.js +++ b/server/uptime-calculator.js @@ -543,10 +543,6 @@ class UptimeCalculator { * @throws {Error} Invalid status */ flatStatus(status) { - if (typeof status !== "number") { - throw new Error("Invalid status: status must be a number"); - } - switch (status) { case UP: case MAINTENANCE: @@ -554,9 +550,8 @@ class UptimeCalculator { case DOWN: case PENDING: return DOWN; - default: - throw new Error("Invalid status: " + status); } + throw new Error("Invalid status"); } /** diff --git a/src/components/Datetime.vue b/src/components/Datetime.vue index 72ed970d9..0d4e182cd 100644 --- a/src/components/Datetime.vue +++ b/src/components/Datetime.vue @@ -7,7 +7,7 @@ export default { props: { /** Value of date time */ value: { - type: [ String, Number ], + type: String, default: null, }, /** Should only the date be displayed? */ diff --git a/src/lang/en.json b/src/lang/en.json index a8796c2cd..98a8f3b63 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1110,8 +1110,5 @@ "Sender name": "Sender name", "smsplanetNeedToApproveName": "Needs to be approved in the client panel", "Disable URL in Notification": "Disable URL in Notification", - "Manual": "Manual", - "Manual Status": "Manual Status", - "Manual status set to": "Manual status set to", - "Status updated": "Status updated" + "Manual": "Manual" } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 8a974e427..7f0bf365d 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -776,7 +776,7 @@
-
+
@@ -787,6 +787,9 @@ {{ $t("Maintenance") }}
+
+ {{ $t("Manual status can be set after monitor is created") }} +
@@ -1229,6 +1232,7 @@ export default { remoteBrowsersEnabled: false, }; }, + computed: { timeoutStep() { return this.monitor.type === "ping" ? 1 : 0.1; @@ -2039,12 +2043,11 @@ message HealthCheckResponse { let updatedMonitor = { ...this.monitor }; updatedMonitor.id = this.monitor.id; - this.$root.getSocket().emit("addHeartbeat", { + this.$root.getSocket().emit("updateManual", { monitorID: this.monitor.id, - status: status === "up" ? 1 : status === "down" ? 0 : status === "maintenance" ? 3 : 2, + status: status === "up" ? 1 : status === "down" ? 0 : 3, msg: status === "up" ? "Up" : status === "down" ? "Down" : "Maintenance", time: new Date().getTime(), - ping: 0 }, (res) => { if (res.ok) { this.toast.success(this.$t("Success"));