Merge branch 'master' into wait-for-analaytics-e2e

This commit is contained in:
Frank Elsinga
2026-01-06 05:48:06 +01:00
committed by GitHub
20 changed files with 210 additions and 62 deletions

View File

@@ -22,6 +22,7 @@ jobs:
contents: read
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-22.04, windows-latest, ubuntu-22.04-arm]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
@@ -41,13 +42,13 @@ jobs:
id: node-modules-cache
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm clean-install --no-fund
- name: Rebuild native modules for ARM64
if: matrix.os == 'ubuntu-22.04-arm'
@@ -65,6 +66,7 @@ jobs:
permissions:
contents: read
strategy:
fail-fast: false
matrix:
node: [ 20, 22 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
@@ -86,8 +88,8 @@ jobs:
docker run --rm --platform linux/arm/v7 \
-v $PWD:/workspace \
-w /workspace \
arm32v7/node:${{ matrix.node }}-slim \
bash -c "npm install --production"
arm32v7/node:${{ matrix.node }} \
npm clean-install --no-fund --production
check-linters:
runs-on: ubuntu-latest
@@ -104,13 +106,13 @@ jobs:
id: node-modules-cache
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js 20
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 20
- run: npm install
- run: npm clean-install --no-fund
- run: npm run lint:prod
e2e-test:
@@ -129,13 +131,13 @@ jobs:
id: node-modules-cache
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 22
- run: npm install
- run: npm clean-install --no-fund
- name: Rebuild native modules for ARM64
run: npm rebuild @louislam/sqlite3

View File

@@ -2,10 +2,22 @@ const { MonitorType } = require("./monitor-type");
const { log, UP } = require("../../src/util");
const mqtt = require("mqtt");
const jsonata = require("jsonata");
const { ConditionVariable } = require("../monitor-conditions/variables");
const { defaultStringOperators, defaultNumberOperators } = require("../monitor-conditions/operators");
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
class MqttMonitorType extends MonitorType {
name = "mqtt";
supportsConditions = true;
conditionVariables = [
new ConditionVariable("topic", defaultStringOperators),
new ConditionVariable("message", defaultStringOperators),
new ConditionVariable("json_value", defaultStringOperators.concat(defaultNumberOperators)),
];
/**
* @inheritdoc
*/
@@ -19,32 +31,98 @@ class MqttMonitorType extends MonitorType {
});
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
// use old default
monitor.mqttCheckType = "keyword";
}
if (monitor.mqttCheckType === "keyword") {
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP;
} else {
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
}
// Check if conditions are defined
const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null;
const hasConditions = conditions && conditions.children && conditions.children.length > 0;
if (hasConditions) {
await this.checkConditions(monitor, heartbeat, messageTopic, receivedMessage, conditions);
} else if (monitor.mqttCheckType === "keyword") {
this.checkKeyword(monitor, heartbeat, messageTopic, receivedMessage);
} else if (monitor.mqttCheckType === "json-query") {
const parsedMessage = JSON.parse(receivedMessage);
let expression = jsonata(monitor.jsonPath);
let result = await expression.evaluate(parsedMessage);
if (result?.toString() === monitor.expectedValue) {
heartbeat.msg = "Message received, expected value is found";
heartbeat.status = UP;
} else {
throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]");
}
await this.checkJsonQuery(monitor, heartbeat, receivedMessage);
} else {
throw Error("Unknown MQTT Check Type");
throw new Error("Unknown MQTT Check Type");
}
}
/**
* Check using keyword matching
* @param {object} monitor Monitor object
* @param {object} heartbeat Heartbeat object
* @param {string} messageTopic Received MQTT topic
* @param {string} receivedMessage Received MQTT message
* @returns {void}
* @throws {Error} If keyword is not found in message
*/
checkKeyword(monitor, heartbeat, messageTopic, receivedMessage) {
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP;
} else {
throw new Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
}
}
/**
* Check using JSONata query
* @param {object} monitor Monitor object
* @param {object} heartbeat Heartbeat object
* @param {string} receivedMessage Received MQTT message
* @returns {Promise<void>}
*/
async checkJsonQuery(monitor, heartbeat, receivedMessage) {
const parsedMessage = JSON.parse(receivedMessage);
const expression = jsonata(monitor.jsonPath);
const result = await expression.evaluate(parsedMessage);
if (result?.toString() === monitor.expectedValue) {
heartbeat.msg = "Message received, expected value is found";
heartbeat.status = UP;
} else {
throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]");
}
}
/**
* Check using conditions system
* @param {object} monitor Monitor object
* @param {object} heartbeat Heartbeat object
* @param {string} messageTopic Received MQTT topic
* @param {string} receivedMessage Received MQTT message
* @param {ConditionExpressionGroup} conditions Parsed conditions
* @returns {Promise<void>}
*/
async checkConditions(monitor, heartbeat, messageTopic, receivedMessage, conditions) {
let jsonValue = null;
// Parse JSON and extract value if jsonPath is defined
if (monitor.jsonPath) {
try {
const parsedMessage = JSON.parse(receivedMessage);
const expression = jsonata(monitor.jsonPath);
jsonValue = await expression.evaluate(parsedMessage);
} catch (e) {
// JSON parsing failed, jsonValue remains null
}
}
const conditionData = {
topic: messageTopic,
message: receivedMessage,
json_value: jsonValue?.toString() ?? "",
};
const conditionsResult = evaluateExpressionGroup(conditions, conditionData);
if (conditionsResult) {
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP;
} else {
throw new Error(`Conditions not met - Topic: ${messageTopic}; Message: ${receivedMessage}`);
}
}

View File

@@ -7,7 +7,7 @@
<h5 class="modal-title">
{{ $t("Add API Key") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<!-- Name -->
@@ -67,7 +67,7 @@
<h5 class="modal-title">
{{ $t("Key Added") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">

View File

@@ -6,7 +6,7 @@
<h5 class="modal-title">
{{ $t("Badge Generator", [monitor.name]) }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -6,17 +6,17 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ title || $t("Confirm") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<slot />
</div>
<div class="modal-footer">
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
{{ yesText }}
{{ yesText || $t("Yes") }}
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="no">
{{ noText }}
{{ noText || $t("No") }}
</button>
</div>
</div>
@@ -37,12 +37,12 @@ export default {
/** Text to use as yes */
yesText: {
type: String,
default: "Yes", // TODO: No idea what to translate this
default: null,
},
/** Text to use as no */
noText: {
type: String,
default: "No",
default: null,
},
/** Title to show on modal. Defaults to translated version of "Config" */
title: {

View File

@@ -6,7 +6,7 @@
<h5 class="modal-title">
{{ $t("New Group") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<form @submit.prevent="confirm">

View File

@@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Docker Host") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -6,7 +6,7 @@
<h5 class="modal-title">
{{ $t("Monitor Setting", [monitor.name]) }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="my-3 form-check">

View File

@@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Notification") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Proxy") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Add a Remote Browser") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -6,10 +6,10 @@
<h5 class="modal-title">
{{ $t("Browser Screenshot") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body"></div>
<img :src="imageURL" alt="screenshot of the website">
<img :src="imageURL" :alt="$t('screenshot of the website')">
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Edit Tag") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -9,7 +9,7 @@
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
</h5>
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')" />
</div>
<div class="modal-body">
<div class="mb-3">

View File

@@ -85,12 +85,12 @@ export default {
title() {
if (this.type === "1y") {
return `1${this.$t("-year")}`;
return `1 ${this.$tc("year", 1)}`;
}
if (this.type === "720") {
return `30${this.$t("-day")}`;
return `30 ${this.$tc("day", 30)}`;
}
return `24${this.$t("-hour")}`;
return `24 ${this.$tc("hour", 24)}`;
}
},
};

View File

@@ -20,7 +20,7 @@
<div
class="btn-group"
role="group"
aria-label="Basic checkbox toggle button group"
:aria-label="$t('Basic checkbox toggle button group')"
>
<input
id="btncheck1"
@@ -69,7 +69,7 @@
<div
class="btn-group"
role="group"
aria-label="Basic checkbox toggle button group"
:aria-label="$t('Basic checkbox toggle button group')"
>
<input
id="btncheck4"

View File

@@ -52,10 +52,8 @@
"now": "now",
"time ago": "{0} ago",
"day": "day | days",
"-day": "-day",
"hour": "hour",
"-hour": "-hour",
"-year": "-year",
"hour": "hour | hours",
"year": "year | years",
"Response": "Response",
"Ping": "Ping",
"Monitor Type": "Monitor Type",
@@ -1121,6 +1119,8 @@
"less than or equal to": "less than or equal to",
"greater than or equal to": "greater than or equal to",
"record": "record",
"message": "message",
"json_value": "JSON value",
"Notification Channel": "Notification Channel",
"Sound": "Sound",
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
@@ -1246,6 +1246,9 @@
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
"lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors.",
"imageResetConfirmation": "Image reset to default",
"screenshot of the website": "Screenshot of the website",
"Basic checkbox toggle button group": "Basic checkbox toggle button group",
"Basic radio toggle button group": "Basic radio toggle button group",
"mtls-auth-server-cert-label": "Cert",
"mtls-auth-server-cert-placeholder": "Cert body",
"mtls-auth-server-key-label": "Key",

View File

@@ -237,7 +237,7 @@
>
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(24{{ $t("-hour") }})
({{ 24 }} {{ $tc("hour", 24) }})
</p>
<span class="col-4 col-sm-12 num">
<CountUp :value="avgPing" />
@@ -250,7 +250,7 @@
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(24{{ $t("-hour") }})
({{ 24 }} {{ $tc("hour", 24) }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="24" />
@@ -263,7 +263,7 @@
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(30{{ $t("-day") }})
({{ 30 }} {{ $tc("day", 30) }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="720" />
@@ -276,7 +276,7 @@
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(1{{ $t("-year") }})
({{ 1 }} {{ $tc("year", 1) }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="1y" />

View File

@@ -33,7 +33,7 @@
{{ $t("setupDatabaseChooseDatabase") }}
</p>
<div class="btn-group" role="group" aria-label="Basic radio toggle button group">
<div class="btn-group" role="group" :aria-label="$t('Basic radio toggle button group')">
<template v-if="info.isEnabledEmbeddedMariaDB">
<input id="btnradio3" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="embedded-mariadb">

View File

@@ -12,9 +12,10 @@ const { UP, PENDING } = require("../../../src/util");
* @param {string} receivedMessage what message is received from the mqtt channel
* @param {string} monitorTopic which MQTT topic is monitored (wildcards are allowed)
* @param {string} publishTopic to which MQTT topic the message is sent
* @param {string|null} conditions JSON string of conditions or null
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, monitorTopic = "test", publishTopic = "test") {
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, monitorTopic = "test", publishTopic = "test", conditions = null) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
@@ -30,6 +31,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, moni
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
conditions: conditions, // for conditions system
};
const heartbeat = {
msg: "",
@@ -157,4 +159,67 @@ describe("MqttMonitorType", {
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
// Conditions system tests
test("check() sets status to UP when message condition matches (contains)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "KEYWORD"
}
]);
const heartbeat = await testMqtt("", null, "-> KEYWORD <-", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("check() sets status to UP when topic condition matches (equals)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "sensors/temp"
}
]);
const heartbeat = await testMqtt("", null, "any message", "sensors/temp", "sensors/temp", conditions);
assert.strictEqual(heartbeat.status, UP);
});
test("check() rejects when message condition does not match", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "EXPECTED"
}
]);
await assert.rejects(
testMqtt("", null, "actual message without keyword", "test", "test", conditions),
new Error("Conditions not met - Topic: test; Message: actual message without keyword")
);
});
test("check() sets status to UP with multiple conditions (AND)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "test"
},
{
type: "expression",
variable: "message",
operator: "contains",
value: "success",
andOr: "and"
}
]);
const heartbeat = await testMqtt("", null, "operation success", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
});
});