This commit is contained in:
XGhozt 2026-01-20 06:03:21 +00:00 committed by GitHub
commit 9d72fae5cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 158 additions and 0 deletions

View File

@ -42,6 +42,7 @@ const {
axiosAbortSignal,
checkCertificateHostname,
} = require("../util-server");
const { Settings } = require("../settings");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@ -441,6 +442,16 @@ class Monitor extends BeanModel {
}
}
/**
* If the monitor designated to represent healthy connectivity is down,
* then we can just stop here.
*/
const systemIsHealthy = await this.systemIsHealthy();
if (systemIsHealthy === false) {
log.warn("monitor", "Health check monitor is down, monitoring paused!");
return;
}
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
@ -2158,6 +2169,42 @@ class Monitor extends BeanModel {
await this.checkCertExpiryNotifications(tlsInfo);
}
}
/**
* Checks if the monitor selected for the health check is down.
* @returns {Promise<boolean>} If true, the system is healthy.
*/
async systemIsHealthy() {
let healthCheckMonitorId = await Settings.get("healthCheckMonitorId");
// User hasn't made a selection yet, save in the database as null
if (healthCheckMonitorId === undefined) {
await setSetting("healthCheckMonitorId", null);
healthCheckMonitorId = null;
}
// No health check monitor is specified, nothing to do!
if (healthCheckMonitorId === null) {
return true;
}
// We still need to check the health check monitor
if (healthCheckMonitorId === this.id) {
return true;
}
const healthCheckMonitor = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
healthCheckMonitorId,
]);
if (healthCheckMonitor) {
return healthCheckMonitor.status === UP;
}
// Default to indicative of being healthy, this shouldn't happen
// Better to be safe if we can't find the selected monitor, it may have been deleted
return true;
}
}
module.exports = Monitor;

View File

@ -0,0 +1,83 @@
<template>
<div v-if="!healthCheckStatus" id="health-check" class="d-flex flex-wrap py-3 mx-4">
<div class="alert alert-danger w-100 d-inline-flex align-items-center justify-content-between">
<div class="px-3">Monitoring is paused, the health check monitor is down!</div>
<div>
<router-link :to="monitorURL(healthCheckMonitor.id)" class="btn btn-danger text-nowrap">
View {{ healthCheckMonitor.name }}
</router-link>
</div>
</div>
</div>
</template>
<script>
import { UP } from "../util.ts";
import { getMonitorRelativeURL } from "../util.ts";
export default {
data() {
return {
settings: {},
};
},
computed: {
/**
* Find the designated health check monitor from the monitor list
* @returns {*|null} A monitor object if the health check monitor id is set
*/
healthCheckMonitor() {
const healthCheckMonitorId = this.settings?.healthCheckMonitorId;
if (this.$root.monitorList[healthCheckMonitorId]) {
return this.$root.monitorList[healthCheckMonitorId];
}
return null;
},
/**
* Determines if the designated health check monitor is down
* @returns {boolean} The health check monitor status
*/
healthCheckStatus() {
const healthCheckMonitorId = this.healthCheckMonitor?.id;
if (
healthCheckMonitorId in this.$root.lastHeartbeatList &&
this.$root.lastHeartbeatList[healthCheckMonitorId]
) {
return this.$root.lastHeartbeatList[healthCheckMonitorId].status === UP;
}
return true;
},
},
mounted() {
this.loadSettings();
},
methods: {
/**
* Load settings from server
* @returns {void}
*/
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
});
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
},
};
</script>

View File

@ -54,6 +54,25 @@
</div>
</div>
<div v-if="settingsLoaded" class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("Heath Check") }}</h5>
<p>{{ $t("HealthCheckDescription") }}</p>
<div class="my-4">
<label for="timezone" class="form-label">
{{ $t("Monitor") }}
</label>
<select id="timezone" v-model="settings.healthCheckMonitorId" class="form-select">
<option :value="null">
{{ $t("Select") }}
</option>
<option v-for="(monitor, index) in $root.monitorList" :key="index" :value="monitor.id">
{{ monitor.name }}
</option>
</select>
</div>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p>

View File

@ -1278,6 +1278,8 @@
"Plain Text": "Plain Text",
"Message Template": "Message Template",
"Template Format": "Template Format",
"Heath Check": "Heath Check",
"HealthCheckDescription": "If the selected monitor is offline, all notifications will be paused and downtime ignored. Ideal for monitoring connectivity to the internet.",
"Font Twemoji by Twitter licensed under": "Font Twemoji by Twitter licensed under",
"smsplanetApiToken": "Token for the SMSPlanet API",
"smsplanetApiDocs": "Detailed information on obtaining API tokens can be found in {the_smsplanet_documentation}.",

View File

@ -126,6 +126,7 @@
</header>
<main>
<health-check-alert v-if="$root.loggedIn" />
<router-view v-if="$root.loggedIn" />
<Login v-if="!$root.loggedIn && $root.allowLoginDialog" />
</main>
@ -169,10 +170,12 @@
import Login from "../components/Login.vue";
import compareVersions from "compare-versions";
import { useToast } from "vue-toastification";
import HealthCheckAlert from "../components/HealthCheckAlert.vue";
const toast = useToast();
export default {
components: {
HealthCheckAlert,
Login,
},

View File

@ -188,6 +188,10 @@ export default {
this.settings.trustProxy = false;
}
if (this.settings.healthCheckMonitorId === undefined) {
this.settings.healthCheckMonitorId = null;
}
this.settingsLoaded = true;
});
},