Merge branch 'master' into XMPP-retry-test

This commit is contained in:
Frank Elsinga 2026-01-06 06:42:44 +01:00 committed by GitHub
commit b4c2624c69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 230 additions and 70 deletions

View File

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

View File

@ -2,10 +2,22 @@ const { MonitorType } = require("./monitor-type");
const { log, UP } = require("../../src/util"); const { log, UP } = require("../../src/util");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const jsonata = require("jsonata"); 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 { class MqttMonitorType extends MonitorType {
name = "mqtt"; name = "mqtt";
supportsConditions = true;
conditionVariables = [
new ConditionVariable("topic", defaultStringOperators),
new ConditionVariable("message", defaultStringOperators),
new ConditionVariable("json_value", defaultStringOperators.concat(defaultNumberOperators)),
];
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -19,32 +31,98 @@ class MqttMonitorType extends MonitorType {
}); });
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") { if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
// use old default
monitor.mqttCheckType = "keyword"; monitor.mqttCheckType = "keyword";
} }
if (monitor.mqttCheckType === "keyword") { // Check if conditions are defined
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) { const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null;
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`; const hasConditions = conditions && conditions.children && conditions.children.length > 0;
heartbeat.status = UP;
} else { if (hasConditions) {
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`); 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") { } else if (monitor.mqttCheckType === "json-query") {
const parsedMessage = JSON.parse(receivedMessage); await this.checkJsonQuery(monitor, heartbeat, 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 + "]");
}
} else { } 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"> <h5 class="modal-title">
{{ $t("Add API Key") }} {{ $t("Add API Key") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<!-- Name --> <!-- Name -->
@ -67,7 +67,7 @@
<h5 class="modal-title"> <h5 class="modal-title">
{{ $t("Key Added") }} {{ $t("Key Added") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">

View File

@ -4,11 +4,16 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
{{ $t("Badge Generator", [monitor.name]) }} {{ $t("Badge Link Generator", [monitor.name]) }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<i18n-t keypath="Badge Link Generator Helptext" tag="p" class="form-text mb-3">
<template #documentation>
<a href="https://github.com/louislam/uptime-kuma/wiki/Badge" target="_blank" rel="noopener noreferrer">{{ $t("documentation") }}</a>
</template>
</i18n-t>
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">{{ $t("Badge Type") }}</label> <label for="type" class="form-label">{{ $t("Badge Type") }}</label>
<select id="type" v-model="badge.type" class="form-select"> <select id="type" v-model="badge.type" class="form-select">

View File

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

View File

@ -6,7 +6,7 @@
<h5 class="modal-title"> <h5 class="modal-title">
{{ $t("New Group") }} {{ $t("New Group") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<form @submit.prevent="confirm"> <form @submit.prevent="confirm">

View File

@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Docker Host") }} {{ $t("Setup Docker Host") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -6,7 +6,7 @@
<h5 class="modal-title"> <h5 class="modal-title">
{{ $t("Monitor Setting", [monitor.name]) }} {{ $t("Monitor Setting", [monitor.name]) }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="my-3 form-check"> <div class="my-3 form-check">
@ -31,10 +31,10 @@
<button <button
class="btn btn-primary btn-add-group me-2" class="btn btn-primary btn-add-group me-2"
@click="$refs.badgeGeneratorDialog.show(monitor.id, monitor.name)" @click="$refs.badgeLinkGeneratorDialog.show(monitor.id, monitor.name)"
> >
<font-awesome-icon icon="certificate" /> <font-awesome-icon icon="certificate" />
{{ $t("Open Badge Generator") }} {{ $t("Open Badge Link Generator") }}
</button> </button>
</div> </div>
@ -46,16 +46,16 @@
</div> </div>
</div> </div>
</div> </div>
<BadgeGeneratorDialog ref="badgeGeneratorDialog" /> <BadgeLinkGeneratorDialog ref="badgeLinkGeneratorDialog" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import BadgeGeneratorDialog from "./BadgeGeneratorDialog.vue"; import BadgeLinkGeneratorDialog from "./BadgeLinkGeneratorDialog.vue";
export default { export default {
components: { components: {
BadgeGeneratorDialog BadgeLinkGeneratorDialog
}, },
props: {}, props: {},
emits: [], emits: [],

View File

@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Notification") }} {{ $t("Setup Notification") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Proxy") }} {{ $t("Setup Proxy") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
{{ $t("Add a Remote Browser") }} {{ $t("Add a Remote Browser") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -6,10 +6,10 @@
<h5 class="modal-title"> <h5 class="modal-title">
{{ $t("Browser Screenshot") }} {{ $t("Browser Screenshot") }}
</h5> </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>
<div class="modal-body"></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> </div>
</div> </div>

View File

@ -7,7 +7,7 @@
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
{{ $t("Edit Tag") }} {{ $t("Edit Tag") }}
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <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 == true" class="badge bg-primary">{{ $t("Active") }}</span>
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span> <span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
</h5> </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>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -85,12 +85,12 @@ export default {
title() { title() {
if (this.type === "1y") { if (this.type === "1y") {
return `1${this.$t("-year")}`; return `1 ${this.$tc("year", 1)}`;
} }
if (this.type === "720") { 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 <div
class="btn-group" class="btn-group"
role="group" role="group"
aria-label="Basic checkbox toggle button group" :aria-label="$t('Basic checkbox toggle button group')"
> >
<input <input
id="btncheck1" id="btncheck1"
@ -69,7 +69,7 @@
<div <div
class="btn-group" class="btn-group"
role="group" role="group"
aria-label="Basic checkbox toggle button group" :aria-label="$t('Basic checkbox toggle button group')"
> >
<input <input
id="btncheck4" id="btncheck4"

View File

@ -52,10 +52,8 @@
"now": "now", "now": "now",
"time ago": "{0} ago", "time ago": "{0} ago",
"day": "day | days", "day": "day | days",
"-day": "-day", "hour": "hour | hours",
"hour": "hour", "year": "year | years",
"-hour": "-hour",
"-year": "-year",
"Response": "Response", "Response": "Response",
"Ping": "Ping", "Ping": "Ping",
"Monitor Type": "Monitor Type", "Monitor Type": "Monitor Type",
@ -911,8 +909,9 @@
"Monitor Setting": "{0}'s Monitor Setting", "Monitor Setting": "{0}'s Monitor Setting",
"Show Clickable Link": "Show Clickable Link", "Show Clickable Link": "Show Clickable Link",
"Show Clickable Link Description": "If checked everyone who have access to this status page can have access to monitor URL.", "Show Clickable Link Description": "If checked everyone who have access to this status page can have access to monitor URL.",
"Open Badge Generator": "Open Badge Generator", "Open Badge Link Generator": "Open Badge Link Generator",
"Badge Generator": "{0}'s Badge Generator", "Badge Link Generator": "{0}'s Badge Link Generator",
"Badge Link Generator Helptext": "Badge links are available for all monitors assigned to public status pages. For more information, please see the {documentation}.",
"Badge Type": "Badge Type", "Badge Type": "Badge Type",
"Badge Duration (in hours)": "Badge Duration (in hours)", "Badge Duration (in hours)": "Badge Duration (in hours)",
"Badge Label": "Badge Label", "Badge Label": "Badge Label",
@ -1121,6 +1120,8 @@
"less than or equal to": "less than or equal to", "less than or equal to": "less than or equal to",
"greater than or equal to": "greater than or equal to", "greater than or equal to": "greater than or equal to",
"record": "record", "record": "record",
"message": "message",
"json_value": "JSON value",
"Notification Channel": "Notification Channel", "Notification Channel": "Notification Channel",
"Sound": "Sound", "Sound": "Sound",
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only", "Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
@ -1246,6 +1247,9 @@
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.", "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.", "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", "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-label": "Cert",
"mtls-auth-server-cert-placeholder": "Cert body", "mtls-auth-server-cert-placeholder": "Cert body",
"mtls-auth-server-key-label": "Key", "mtls-auth-server-key-label": "Key",

View File

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

View File

@ -33,7 +33,7 @@
{{ $t("setupDatabaseChooseDatabase") }} {{ $t("setupDatabaseChooseDatabase") }}
</p> </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"> <template v-if="info.isEnabledEmbeddedMariaDB">
<input id="btnradio3" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="embedded-mariadb"> <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} receivedMessage what message is received from the mqtt channel
* @param {string} monitorTopic which MQTT topic is monitored (wildcards are allowed) * @param {string} monitorTopic which MQTT topic is monitored (wildcards are allowed)
* @param {string} publishTopic to which MQTT topic the message is sent * @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 * @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 hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString(); const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType(); const mqttMonitorType = new MqttMonitorType();
@ -30,6 +31,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, moni
mqttSuccessMessage: mqttSuccessMessage, // for keywords mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType, mqttCheckType: mqttCheckType,
conditions: conditions, // for conditions system
}; };
const heartbeat = { const heartbeat = {
msg: "", msg: "",
@ -157,4 +159,67 @@ describe("MqttMonitorType", {
new Error("Message received but value is not equal to expected value, value was: [present]") 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);
});
}); });

View File

@ -178,6 +178,9 @@ test.describe("Status Page", () => {
await page.getByTestId("analytics-id-input").fill(plausibleAnalyticsDomainsUrls); await page.getByTestId("analytics-id-input").fill(plausibleAnalyticsDomainsUrls);
await page.getByTestId("save-button").click(); await page.getByTestId("save-button").click();
await screenshot(testInfo, page); await screenshot(testInfo, page);
await page.waitForFunction((scriptUrl) => {
return document.head.innerHTML.includes(scriptUrl);
}, plausibleAnalyticsScriptUrl, { timeout: 5000 });
expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsScriptUrl); expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsScriptUrl);
expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainsUrls); expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainsUrls);
@ -188,6 +191,9 @@ test.describe("Status Page", () => {
await page.getByTestId("analytics-id-input").fill(matomoSiteId); await page.getByTestId("analytics-id-input").fill(matomoSiteId);
await page.getByTestId("save-button").click(); await page.getByTestId("save-button").click();
await screenshot(testInfo, page); await screenshot(testInfo, page);
await page.waitForFunction((url) => {
return document.head.innerHTML.includes(url);
}, matomoUrl, { timeout: 5000 });
expect(await page.locator("head").innerHTML()).toContain(matomoUrl); expect(await page.locator("head").innerHTML()).toContain(matomoUrl);
expect(await page.locator("head").innerHTML()).toContain(matomoSiteId); expect(await page.locator("head").innerHTML()).toContain(matomoSiteId);
}); });