Merge pull request #2961 from bluewave-labs/develop

develop -> demo
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-09-18 12:04:14 -07:00
commit ce2808ccbc
17 files changed
+1361 -54

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ import { URLSearchParams } from "url";
const API_TOKEN = process.env.POEDITOR_API;
const PROJECT_ID = process.env.POEDITOR_PROJECT_ID;
const LANGUAGES = (
process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn"
process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
).split(",");
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
+1 -1
View File
@@ -7,7 +7,7 @@ on:
languages:
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
required: false
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn"
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
format:
description: "Export format (key_value_json or json)"
required: false
+1 -1
View File
@@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
### Using a Custom CA
+22 -8
View File
@@ -45,7 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "6.3.5"
"vite": "6.3.6"
}
},
"node_modules/@ampproject/remapping": {
@@ -2422,13 +2422,13 @@
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -3756,6 +3756,20 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6375,9 +6389,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
+1 -1
View File
@@ -58,6 +58,6 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "6.3.5"
"vite": "6.3.6"
}
}
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useCallback } from "react";
const useInfrastructureMonitorForm = () => {
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
@@ -6,6 +6,8 @@ const useInfrastructureMonitorForm = () => {
notifications: [],
notify_email: false,
interval: 0.25,
statusWindowSize: 5,
statusWindowThreshold: 60,
cpu: false,
usage_cpu: "",
memory: false,
@@ -29,9 +31,10 @@ const useInfrastructureMonitorForm = () => {
[event.target.name]: event.target.checked,
});
};
const initializeInfrastructureMonitorForCreate = (globalSettings) => {
const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
const gt = globalSettings?.data?.settings?.globalThresholds || {};
setInfrastructureMonitor({
setInfrastructureMonitor((prev) => ({
...prev,
url: "",
name: "",
notifications: [],
@@ -45,17 +48,20 @@ const useInfrastructureMonitorForm = () => {
temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
});
};
}));
}, []);
const initializeInfrastructureMonitorForUpdate = (monitor) => {
const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => {
const MS_PER_MINUTE = 60000;
const { thresholds = {} } = monitor;
setInfrastructureMonitor({
setInfrastructureMonitor((prev) => ({
...prev,
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
cpu: thresholds.usage_cpu !== undefined,
usage_cpu:
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
@@ -75,8 +81,8 @@ const useInfrastructureMonitorForm = () => {
? (thresholds.usage_temperature * 100).toString()
: "",
secret: monitor.secret || "",
});
};
}));
}, []);
return {
infrastructureMonitor,
setInfrastructureMonitor,
@@ -12,6 +12,8 @@ const useInfrastructureSubmit = () => {
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
statusWindowSize: infrastructureMonitor.statusWindowSize,
statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
@@ -86,7 +86,14 @@ const CreateInfrastructureMonitor = () => {
setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(monitor);
}
}, [isCreate, monitor, globalSettings, globalSettingsLoading]);
}, [
isCreate,
monitor,
globalSettings,
globalSettingsLoading,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
]);
// Handlers
const onSubmit = async (event) => {
@@ -281,6 +288,39 @@ const CreateInfrastructureMonitor = () => {
setNotifications={infrastructureMonitor.notifications}
/>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={infrastructureMonitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={infrastructureMonitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<CustomAlertsSection
errors={errors}
onChange={onChange}
+10
View File
@@ -437,6 +437,16 @@ const infrastructureMonitorValidation = joi.object({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
statusWindowSize: joi.number().min(1).max(20).messages({
"number.base": "Status window size must be a number.",
"number.min": "Status window size must be at least 1.",
"number.max": "Status window size cannot exceed 20.",
}),
statusWindowThreshold: joi.number().min(1).max(100).messages({
"number.base": "Status window threshold must be a number.",
"number.min": "Status window threshold must be at least 1%.",
"number.max": "Status window threshold cannot exceed 100%.",
}),
notifications: joi.array().items(joi.string()),
});
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -5,12 +5,12 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
let version = "3.1.8";
let version = "3.2.0";
return {
base: "/",
plugins: [svgr(), react()],
optimizeDeps: { Maybe this will be the straw th
optimizeDeps: {
include: ["@mui/material/Tooltip", "@emotion/styled"],
},
define: {
+16 -1
View File
@@ -149,10 +149,12 @@
"statusPageByTeamId": "Got status pages by team id successfully",
"httpNetworkError": "Network error",
"httpNotJson": "Response data is not json",
"httpExpectedValueFail": "Expected value did not match",
"httpJsonPathError": "Failed to parse json data",
"httpEmptyResult": "Result is empty",
"httpMatchSuccess": "Response data match successfully",
"httpMatchFail": "Failed to match response data",
"httpJsonPathFail": "JSONPath did not match expected value",
"webhookSendSuccess": "Webhook notification sent successfully",
"telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId",
"webhookUrlRequired": "Webhook URL is required",
@@ -162,5 +164,18 @@
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test E-mail from Checkmate"
"testEmailSubject": "Test E-mail from Checkmate",
"discordNotification": {
"monitor": "Monitor",
"status": "Status",
"statusCode": "Status Code",
"time": "Time",
"up": "UP",
"down": "DOWN",
"checkmate": "Checkmate",
"url": "URL",
"unknown": "Unknown",
"uptimeAlert": "Uptime Alert: One of your monitors is back online",
"downtimeAlert": "Downtime Alert: One of your monitors went offline"
}
}
@@ -28,11 +28,12 @@ class SuperSimpleQueueHelper {
return async (monitor) => {
try {
const monitorId = monitor._id;
const teamId = monitor.teamId;
if (!monitorId) {
throw new Error("No monitor id");
}
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
@@ -77,8 +78,11 @@ class SuperSimpleQueueHelper {
};
};
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId);
async isInMaintenanceWindow(monitorId, teamId) {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
monitorId: monitorId.toString(),
teamId: teamId.toString(),
});
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
@@ -1,5 +1,3 @@
import { match } from "assert";
const SERVICE_NAME = "NetworkService";
class NetworkService {
@@ -107,7 +105,7 @@ class NetworkService {
}
async requestHttp(monitor) {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = {
monitorId: _id,
teamId: teamId,
@@ -156,33 +154,61 @@ class NetworkService {
return httpResponse;
}
if (expectedValue) {
if (expectedValue && !jsonPath) {
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include") ok = payload.includes(expectedValue);
if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload);
if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
httpResponse.message = this.stringService.httpExpectedValueFail;
return httpResponse;
}
}
if (jsonPath) {
const contentType = response.headers["content-type"];
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = this.stringService.httpNotJson;
return httpResponse;
}
try {
this.jmespath.search(payload, jsonPath);
const extracted = this.jmespath.search(payload, jsonPath);
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = extracted === expectedValue;
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
if (ok) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
} else {
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
if (!isFalsey) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
}
} catch {
httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError;
@@ -16,7 +16,7 @@ class NotificationService {
return NotificationService.SERVICE_NAME;
}
sendNotification = async ({ notification, subject, content, html }) => {
sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => {
const { type, address } = notification;
if (type === "email") {
@@ -28,9 +28,11 @@ class NotificationService {
// Create a body for webhooks
let body = { text: content };
if (type === "discord") {
body = { content };
body = !discordContent ? { content } : discordContent;
}
if (type === "webhook") {
body = !webhookBody ? { content } : webhookBody;
}
if (type === "slack" || type === "discord" || type === "webhook") {
const response = await this.networkService.requestWebhook(type, address, body);
return response.status;
@@ -55,7 +57,6 @@ class NotificationService {
const notificationIDs = networkResponse.monitor?.notifications ?? [];
if (notificationIDs.length === 0) return false;
if (networkResponse.monitor.type === "hardware") {
const thresholds = networkResponse?.monitor?.thresholds;
@@ -63,30 +64,29 @@ class NotificationService {
const metrics = networkResponse?.payload?.data ?? null;
if (metrics === null) return false; // No metrics, we're done
const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse);
if (alerts.length === 0) return false;
const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts);
const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts);
const success = await this.notifyAll({ notificationIDs, subject, html, content });
const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor);
const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor);
const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody });
return success;
}
// Status monitors
const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse);
const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
const success = this.notifyAll({ notificationIDs, subject, html, content });
const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse);
const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent });
return success;
}
async notifyAll({ notificationIDs, subject, html, content }) {
async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) {
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
// Map each notification to a test promise
const promises = notifications.map(async (notification) => {
try {
await this.sendNotification({ notification, subject, content, html });
await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody });
return true;
} catch (err) {
return false;
@@ -70,7 +70,25 @@ class NotificationUtils {
.replace("{time}", formattedTime)
.replace("{code}", code || "Unknown");
}
return messageText;
const dn = this.stringService.discordNotification;
let discordMessageText = {
embeds: [
{
title: status ? dn.uptimeAlert : dn.downtimeAlert,
color: status ? 5763719 : 15548997,
fields: [
{ name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true },
{ name: dn.status, value: status ? dn.up : dn.down, inline: true },
{ name: dn.statusCode, value: String(code ?? dn.unknown), inline: true },
{ name: dn.time, value: formattedTime, inline: true },
{ name: dn.url, value: monitor?.url ?? dn.unknown, inline: false },
],
footer: { text: dn.checkmate },
},
],
};
return [messageText, discordMessageText];
};
buildHardwareAlerts = async (networkResponse) => {
@@ -88,6 +106,43 @@ class NotificationUtils {
};
const alertsToSend = [];
const discordEmbeds = [];
const monitorInfoFields = [
{ name: "Monitor", value: monitor.name, inline: true },
{ name: "URL", value: monitor.url, inline: false },
];
const formatDiscordAlert = {
cpu: () => ({
title: "CPU alert",
description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
color: 15548997,
fields: monitorInfoFields,
footer: { text: "Checkmate" },
}),
memory: () => ({
title: "Memory alert",
description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
color: 15548997,
fields: monitorInfoFields,
footer: { text: "Checkmate" },
}),
disk: () => ({
title: "Disk alert",
description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
color: 15548997,
footer: { text: "Checkmate" },
fields: [
...monitorInfoFields,
...(Array.isArray(disk) ? disk : []).map((d, idx) => ({
name: `Disk ${idx}`,
value: `${(d?.usage_percent * 100).toFixed(0)}%`,
inline: true,
})),
],
}),
};
const alertTypes = ["cpu", "memory", "disk"];
for (const type of alertTypes) {
// Iterate over each alert type to see if any need to be decremented
@@ -108,11 +163,13 @@ class NotificationUtils {
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
};
alertsToSend.push(formatAlert[type]());
discordEmbeds.push(formatDiscordAlert[type]());
}
}
}
await monitor.save();
return alertsToSend;
const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
return [alertsToSend, discordPayload];
};
buildHardwareEmail = async (networkResponse, alerts) => {
@@ -124,8 +181,15 @@ class NotificationUtils {
return { subject, html };
};
buildHardwareNotificationMessage = (alerts) => {
return alerts.map((alert) => alert).join("\n");
buildHardwareNotificationMessage = (alerts, monitor) => {
const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts] : [];
return alertText.map((alert) => alert).join("\n");
};
buildHardwareWebhookBody = (alerts, monitor) => {
const content = alerts.map((alert) => alert).join("\n");
const body = { text: content, name: monitor.name, url: monitor.url };
return body;
};
}
@@ -216,6 +216,10 @@ class StringService {
return this.translationService.getTranslation("monitorDownAlert");
}
get discordNotification() {
return this.translationService.getTranslation("discordNotification");
}
getWebhookUnsupportedPlatform(platform) {
return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform);
}