Compare commits

...

3 Commits

Author SHA1 Message Date
Evgeniy Timokhov
8d48ed7850 feat: Disabled telegram links preview (#6335) 2025-11-10 21:26:36 +01:00
Teodor Moquist
751ffd8e72 feat: Added option to clone a existing maintenance (#6330) 2025-11-10 19:22:14 +01:00
MayMeow
81544c8a39 Fix Group monitors to send notification after reaching maximum retires count (#6286)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-11-09 17:57:20 +01:00
6 changed files with 96 additions and 34 deletions

View File

@@ -11,35 +11,67 @@ class GroupMonitorType extends MonitorType {
async check(monitor, heartbeat, _server) {
const children = await Monitor.getChildren(monitor.id);
if (children.length > 0) {
heartbeat.status = UP;
heartbeat.msg = "All children up and running";
for (const child of children) {
if (!child.active) {
// Ignore inactive childs
continue;
}
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
// lastBeat.status could be null
if (!lastBeat) {
heartbeat.status = PENDING;
} else if (heartbeat.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
heartbeat.status = lastBeat.status;
} else if (heartbeat.status === PENDING && lastBeat.status === DOWN) {
heartbeat.status = lastBeat.status;
}
}
if (heartbeat.status !== UP) {
heartbeat.msg = "Child inaccessible";
}
} else {
if (children.length === 0) {
// Set status pending if group is empty
heartbeat.status = PENDING;
heartbeat.msg = "Group empty";
return;
}
let worstStatus = UP;
const downChildren = [];
const pendingChildren = [];
for (const child of children) {
if (!child.active) {
// Ignore inactive (=paused) children
continue;
}
const label = child.name || `#${child.id}`;
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
if (!lastBeat) {
if (worstStatus === UP) {
worstStatus = PENDING;
}
pendingChildren.push(label);
continue;
}
if (lastBeat.status === DOWN) {
worstStatus = DOWN;
downChildren.push(label);
} else if (lastBeat.status === PENDING) {
if (worstStatus !== DOWN) {
worstStatus = PENDING;
}
pendingChildren.push(label);
}
}
if (worstStatus === UP) {
heartbeat.status = UP;
heartbeat.msg = "All children up and running";
return;
}
if (worstStatus === PENDING) {
heartbeat.status = PENDING;
heartbeat.msg = `Pending child monitors: ${pendingChildren.join(", ")}`;
return;
}
heartbeat.status = DOWN;
let message = `Child monitors down: ${downChildren.join(", ")}`;
if (pendingChildren.length > 0) {
message += `; pending: ${pendingChildren.join(", ")}`;
}
// Throw to leverage the generic retry handling and notification flow
throw new Error(message);
}
}

View File

@@ -17,6 +17,7 @@ class Telegram extends NotificationProvider {
text: msg,
disable_notification: notification.telegramSendSilently ?? false,
protect_content: notification.telegramProtectContent ?? false,
link_preview_options: { is_disabled: true },
};
if (notification.telegramMessageThreadID) {
params.message_thread_id = notification.telegramMessageThreadID;
@@ -30,9 +31,9 @@ class Telegram extends NotificationProvider {
}
}
let config = this.getAxiosConfigWithProxy({ params });
let config = this.getAxiosConfigWithProxy();
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, config);
await axios.post(`${url}/bot${notification.telegramBotToken}/sendMessage`, params, config);
return okMsg;
} catch (error) {

View File

@@ -518,6 +518,12 @@
"Effective Date Range": "Effective Date Range (Optional)",
"Schedule Maintenance": "Schedule Maintenance",
"Edit Maintenance": "Edit Maintenance",
"Clone Maintenance": "Clone Maintenance",
"ariaPauseMaintenance": "Pause this maintenance schedule",
"ariaResumeMaintenance": "Resume this maintenance schedule",
"ariaCloneMaintenance": "Create a copy of this maintenance schedule",
"ariaEditMaintenance": "Edit this maintenance schedule",
"ariaDeleteMaintenance": "Delete this maintenance schedule",
"Date and Time": "Date and Time",
"DateTime Range": "DateTime Range",
"loadingError": "Cannot fetch the data, please try again later.",

View File

@@ -354,7 +354,14 @@ export default {
},
pageName() {
return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
let name = "Schedule Maintenance";
if (this.isEdit) {
name = "Edit Maintenance";
} else if (this.isClone) {
name = "Clone Maintenance";
}
return this.$t(name);
},
isAdd() {
@@ -365,6 +372,9 @@ export default {
return this.$route.path.startsWith("/maintenance/edit");
},
isClone() {
return this.$route.path.startsWith("/maintenance/clone");
}
},
watch: {
"$route.fullPath"() {
@@ -443,11 +453,16 @@ export default {
daysOfMonth: [],
timezoneOption: null,
};
} else if (this.isEdit) {
} else if (this.isEdit || this.isClone) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
this.maintenance = res.maintenance;
if (this.isClone) {
this.maintenance.id = undefined; // Remove id when cloning as we want a new id
this.maintenance.title = this.$t("cloneOf", [ this.maintenance.title ]);
}
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
Object.values(res.monitors).map(monitor => {
@@ -491,7 +506,7 @@ export default {
return this.processing = false;
}
if (this.isAdd) {
if (this.isAdd || this.isClone) {
this.$root.addMaintenance(this.maintenance, async (res) => {
if (res.ok) {
await this.addMonitorMaintenance(res.maintenanceID, async () => {

View File

@@ -41,19 +41,23 @@
<router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="pauseDialog(item.id)">
<button v-if="item.active" class="btn btn-normal" :aria-label="$t('ariaPauseMaintenance')" @click="pauseDialog(item.id)">
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
<button v-if="!item.active" class="btn btn-primary" :aria-label="$t('ariaResumeMaintenance')" @click="resumeMaintenance(item.id)">
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button>
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
<router-link :to="'/maintenance/clone/' + item.id" class="btn btn-normal" :aria-label="$t('ariaCloneMaintenance')">
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
</router-link>
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal" :aria-label="$t('ariaEditMaintenance')">
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link>
<button class="btn btn-normal text-danger" @click="deleteDialog(item.id)">
<button class="btn btn-normal text-danger" :aria-label="$t('ariaDeleteMaintenance')" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
</div>

View File

@@ -162,6 +162,10 @@ const routes = [
path: "/maintenance/edit/:id",
component: EditMaintenance,
},
{
path: "/maintenance/clone/:id",
component: EditMaintenance,
}
],
},
],