Skip to content
Merged
12 changes: 12 additions & 0 deletions db/knex_migrations/2025-06-11-0000-add-manual-monitor.js
Original file line number Diff line number Diff line change
@@ -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");
});
};
36 changes: 36 additions & 0 deletions server/monitor-types/manual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN, PENDING, MAINTENANCE } = require("../../src/util");

class ManualMonitorType extends MonitorType {
name = "Manual";
type = "manual";
description = "A monitor that allows manual control of the status";
supportsConditions = false;
conditionVariables = [];

/**
* 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
* @returns {Promise<void>}
*/
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;
}
}
}

module.exports = {
ManualMonitorType
};
44 changes: 43 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -1598,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");
Expand Down
2 changes: 2 additions & 0 deletions server/uptime-kuma-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();

// Allow all CORS origins (polling) in development
let cors = undefined;
Expand Down Expand Up @@ -558,4 +559,5 @@ const { GroupMonitorType } = require("./monitor-types/group");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { ManualMonitorType } = require("./monitor-types/manual");
const Monitor = require("./model/monitor");
3 changes: 2 additions & 1 deletion src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1109,5 +1109,6 @@
"Phone numbers": "Phone numbers",
"Sender name": "Sender name",
"smsplanetNeedToApproveName": "Needs to be approved in the client panel",
"Disable URL in Notification": "Disable URL in Notification"
"Disable URL in Notification": "Disable URL in Notification",
"Manual": "Manual"
}
43 changes: 42 additions & 1 deletion src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
<option value="push">
Push
</option>
<option value="manual">
{{ $t("Manual") }}
</option>
</optgroup>

<optgroup :label="$t('Specific Monitor Type')">
Expand Down Expand Up @@ -770,6 +773,24 @@
<div class="my-3">
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
</div>

<div v-if="monitor.type === 'manual'" class="mb-3">
<label class="form-label">{{ $t("Manual Status") }}</label>
<div v-if="!isAdd" class="btn-group w-100 mb-3">
<button class="btn btn-success" @click="setManualStatus('up')">
<i class="fas fa-check"></i> {{ $t("Up") }}
</button>
<button class="btn btn-danger" @click="setManualStatus('down')">
<i class="fas fa-times"></i> {{ $t("Down") }}
</button>
<button class="btn btn-warning" @click="setManualStatus('maintenance')">
<i class="fas fa-tools"></i> {{ $t("Maintenance") }}
</button>
</div>
<div v-else class="alert alert-secondary">
{{ $t("Manual status can be set after monitor is created") }}
</div>
</div>
</div>

<div class="col-md-6">
Expand Down Expand Up @@ -1181,7 +1202,10 @@ export default {
VueMultiselect,
EditMonitorConditions,
},

setup() {
const toast = useToast();
return { toast };
},
data() {
return {
minInterval: MIN_INTERVAL_SECOND,
Expand Down Expand Up @@ -2015,6 +2039,23 @@ message HealthCheckResponse {
}
},

setManualStatus(status) {
let updatedMonitor = { ...this.monitor };
updatedMonitor.id = this.monitor.id;

this.$root.getSocket().emit("updateManual", {
monitorID: this.monitor.id,
status: status === "up" ? 1 : status === "down" ? 0 : 3,
msg: status === "up" ? "Up" : status === "down" ? "Down" : "Maintenance",
time: new Date().getTime(),
}, (res) => {
if (res.ok) {
this.toast.success(this.$t("Success"));
} else {
this.toast.error(res.msg);
}
});
},
},
};
</script>
Expand Down
Loading