diff --git a/data/acknowledgements.json b/data/acknowledgements.json
index 295eeaaed..4bd65e0ad 100644
--- a/data/acknowledgements.json
+++ b/data/acknowledgements.json
@@ -1,6 +1,6 @@
{
"guest_down_endpoint_2_pi_102": {
- "id": "alert_1749383901524_ii0r1wvj6",
+ "id": "alert_1749506759853_8lji2gh42",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -18,110 +18,77 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "maxmem": 536870912,
- "status": "stopped",
- "mem": 0,
- "cpus": 1,
- "swap": 0,
- "tags": "proxmox-helper-scripts",
- "cpu": 0,
- "diskwrite": 0,
- "netout": 0,
- "vmid": 102,
- "maxswap": 536870912,
- "type": "lxc",
- "diskread": 0,
- "uptime": 0,
- "netin": 0,
- "maxdisk": 4401922048,
- "disk": 0,
- "name": "pi-docker",
- "node": "pi",
"endpointId": "endpoint_2",
- "id": "endpoint_2-pi-102",
- "nodeDisplayName": "pimox"
- },
- "startTime": 1749383901523,
- "lastUpdate": 1749383916078,
- "triggeredAt": 1749383901523,
- "currentValue": "stopped",
- "effectiveThreshold": "stopped",
- "state": "active",
- "escalated": false,
- "acknowledged": true,
- "acknowledgedBy": "web-user",
- "acknowledgedAt": 1749383916175,
- "acknowledgeNote": "Acknowledged via web interface",
- "note": "Acknowledged via web interface"
- },
- "guest_down_primary_desktop_400": {
- "id": "alert_1749383901523_u7e6l7r1v",
- "rule": {
- "id": "guest_down",
- "name": "Guest System Down",
- "description": "Virtual machine or container has stopped unexpectedly",
- "metric": "status",
- "condition": "equals",
- "threshold": "stopped",
- "duration": 60000,
- "severity": "critical",
- "enabled": true,
- "tags": [
- "availability",
- "guest"
- ],
- "group": "availability_alerts",
- "escalationTime": 600000,
- "autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
- },
- "guest": {
- "diskread": 0,
- "name": "ubuntu-gpu-vm",
- "maxmem": 2147483648,
- "diskwrite": 0,
- "disk": 0,
- "mem": 0,
- "vmid": 400,
+ "node": "pi",
+ "vmid": 102,
+ "name": "pi-docker",
+ "type": "lxc",
"status": "stopped",
- "cpus": 2,
- "uptime": 0,
- "netin": 0,
- "maxdisk": 34359738368,
- "cpu": 0,
- "netout": 0,
- "node": "desktop",
- "endpointId": "primary",
- "type": "qemu",
- "id": "primary-desktop-400",
- "nodeDisplayName": "homelab - desktop"
+ "maxmem": 536870912,
+ "maxdisk": 4401922048
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923304,
+ "acknowledgedAt": 1749506768114,
+ "acknowledgeNote": "Bulk acknowledged via dropdown",
+ "note": "Bulk acknowledged via dropdown"
+ },
+ "guest_down_primary_desktop_400": {
+ "id": "alert_1749506759852_1huz9md0u",
+ "rule": {
+ "id": "guest_down",
+ "name": "Guest System Down",
+ "description": "Virtual machine or container has stopped unexpectedly",
+ "metric": "status",
+ "condition": "equals",
+ "threshold": "stopped",
+ "duration": 60000,
+ "severity": "critical",
+ "enabled": true,
+ "tags": [
+ "availability",
+ "guest"
+ ],
+ "group": "availability_alerts",
+ "escalationTime": 600000,
+ "autoResolve": true,
+ "suppressionTime": 120000
+ },
+ "guest": {
+ "endpointId": "primary",
+ "node": "desktop",
+ "vmid": 400,
+ "name": "ubuntu-gpu-vm",
+ "type": "qemu",
+ "status": "stopped",
+ "maxmem": 2147483648,
+ "maxdisk": 34359738368
+ },
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
+ "currentValue": "stopped",
+ "effectiveThreshold": "stopped",
+ "state": "active",
+ "escalated": false,
+ "acknowledged": true,
+ "acknowledgedBy": "bulk-operation",
+ "acknowledgedAt": 1749506768076,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
"guest_down_primary_desktop_102": {
- "id": "alert_1749383901524_5sdux0hpp",
+ "id": "alert_1749569747925_cr09csaa4",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -139,49 +106,33 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "diskwrite": 0,
- "name": "windows11",
- "maxmem": 8589934592,
- "diskread": 0,
- "vmid": 102,
- "disk": 0,
- "mem": 0,
- "cpus": 4,
- "status": "stopped",
- "serial": 1,
- "cpu": 0,
- "netout": 0,
- "maxdisk": 68719476736,
- "netin": 0,
- "uptime": 0,
- "node": "desktop",
"endpointId": "primary",
+ "node": "desktop",
+ "vmid": 102,
+ "name": "windows11",
"type": "qemu",
- "id": "primary-desktop-102",
- "nodeDisplayName": "homelab - desktop"
+ "status": "stopped",
+ "maxmem": 8589934592,
+ "maxdisk": 68719476736
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749569747925,
+ "lastUpdate": 1749569831486,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
- "acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923312,
- "acknowledgeNote": "Bulk acknowledged via dropdown",
- "note": "Bulk acknowledged via dropdown"
+ "triggeredAt": 1749569809584,
+ "acknowledgedBy": "web-user",
+ "acknowledgedAt": 1749569832402,
+ "acknowledgeNote": "Acknowledged via web interface",
+ "note": "Acknowledged via web interface"
},
"guest_down_primary_desktop_200": {
- "id": "alert_1749383901524_wc8c7l96a",
+ "id": "alert_1749569747925_g5yxjcnmv",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -199,48 +150,33 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "diskread": 0,
- "maxmem": 8589934592,
- "name": "UnraidServer",
- "diskwrite": 0,
- "mem": 0,
- "disk": 0,
- "vmid": 200,
- "cpus": 6,
- "status": "stopped",
- "uptime": 0,
- "netin": 0,
- "maxdisk": 0,
- "netout": 0,
- "cpu": 0,
- "node": "desktop",
"endpointId": "primary",
+ "node": "desktop",
+ "vmid": 200,
+ "name": "UnraidServer",
"type": "qemu",
- "id": "primary-desktop-200",
- "nodeDisplayName": "homelab - desktop"
+ "status": "stopped",
+ "maxmem": 8589934592,
+ "maxdisk": 0
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749569747925,
+ "lastUpdate": 1749569829318,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
- "acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923314,
- "acknowledgeNote": "Bulk acknowledged via dropdown",
- "note": "Bulk acknowledged via dropdown"
+ "triggeredAt": 1749569809584,
+ "acknowledgedBy": "web-user",
+ "acknowledgedAt": 1749569829548,
+ "acknowledgeNote": "Acknowledged via web interface",
+ "note": "Acknowledged via web interface"
},
"guest_down_primary_desktop_109": {
- "id": "alert_1749383901524_884o95b7f",
+ "id": "alert_1749506759852_h7fxa1ycd",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -258,51 +194,33 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "vmid": 109,
- "mem": 0,
- "disk": 0,
- "type": "lxc",
- "maxswap": 536870912,
- "diskwrite": 0,
- "maxmem": 4294967296,
- "name": "pbs2",
- "diskread": 0,
- "netout": 0,
- "cpu": 0,
- "maxdisk": 10737418240,
- "tags": "backup;community-script",
- "netin": 0,
- "uptime": 0,
- "status": "stopped",
- "cpus": 4,
- "swap": 0,
- "node": "desktop",
"endpointId": "primary",
- "id": "primary-desktop-109",
- "nodeDisplayName": "homelab - desktop"
+ "node": "desktop",
+ "vmid": 109,
+ "name": "pbs2",
+ "type": "lxc",
+ "status": "stopped",
+ "maxmem": 4294967296,
+ "maxdisk": 10737418240
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923317,
+ "acknowledgedAt": 1749506768101,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
"guest_down_primary_desktop_112": {
- "id": "alert_1749383901524_e6h94me1n",
+ "id": "alert_1749506759852_xx9hn6622",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -320,51 +238,33 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "status": "stopped",
- "cpus": 1,
- "swap": 0,
- "tags": "community-script;monitoring;proxmox",
- "maxdisk": 4294967296,
- "cpu": 0,
- "netout": 0,
- "uptime": 0,
- "netin": 0,
- "diskwrite": 0,
- "maxswap": 536870912,
- "type": "lxc",
- "diskread": 0,
- "name": "pulse",
- "maxmem": 1073741824,
- "disk": 0,
- "mem": 0,
- "vmid": 112,
- "node": "desktop",
"endpointId": "primary",
- "id": "primary-desktop-112",
- "nodeDisplayName": "homelab - desktop"
+ "node": "desktop",
+ "vmid": 112,
+ "name": "pulse",
+ "type": "lxc",
+ "status": "stopped",
+ "maxmem": 1073741824,
+ "maxdisk": 4294967296
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923320,
+ "acknowledgedAt": 1749506768098,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
"guest_down_endpoint_2_pi_101": {
- "id": "alert_1749383901524_ew3vhj38x",
+ "id": "alert_1749506759853_fqm539ogq",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -382,51 +282,33 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "diskwrite": 0,
- "cpu": 0,
- "tags": "alpine;community-script;os",
- "swap": 0,
- "cpus": 1,
- "mem": 0,
- "maxmem": 536870912,
- "status": "stopped",
- "disk": 0,
- "name": "pi-influxdb",
- "maxdisk": 2254438400,
- "netin": 0,
- "diskread": 0,
- "uptime": 0,
- "type": "lxc",
- "maxswap": 536870912,
- "vmid": 101,
- "netout": 0,
- "node": "pi",
"endpointId": "endpoint_2",
- "id": "endpoint_2-pi-101",
- "nodeDisplayName": "pimox"
+ "node": "pi",
+ "vmid": 101,
+ "name": "pi-influxdb",
+ "type": "lxc",
+ "status": "stopped",
+ "maxmem": 536870912,
+ "maxdisk": 2254438400
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923322,
+ "acknowledgedAt": 1749506768110,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
"guest_down_endpoint_2_pi_100": {
- "id": "alert_1749383901524_z3xuybdjm",
+ "id": "alert_1749506759853_efv9jpkmk",
"rule": {
"id": "guest_down",
"name": "Guest System Down",
@@ -444,46 +326,28 @@
"group": "availability_alerts",
"escalationTime": 600000,
"autoResolve": true,
- "suppressionTime": 120000,
- "notificationChannels": [
- "default",
- "urgent"
- ]
+ "suppressionTime": 120000
},
"guest": {
- "tags": "proxmox-helper-scripts",
- "swap": 0,
- "cpus": 1,
- "mem": 0,
+ "endpointId": "endpoint_2",
+ "node": "pi",
+ "vmid": 100,
+ "name": "pihole",
+ "type": "lxc",
"status": "stopped",
"maxmem": 536870912,
- "diskwrite": 0,
- "cpu": 0,
- "type": "lxc",
- "maxswap": 536870912,
- "vmid": 100,
- "netout": 0,
- "disk": 0,
- "name": "pihole",
- "maxdisk": 6442450944,
- "netin": 0,
- "diskread": 0,
- "uptime": 0,
- "node": "pi",
- "endpointId": "endpoint_2",
- "id": "endpoint_2-pi-100",
- "nodeDisplayName": "pimox"
+ "maxdisk": 6442450944
},
- "startTime": 1749383901523,
- "lastUpdate": 1749383922611,
- "triggeredAt": 1749383901523,
+ "startTime": 1749506759851,
+ "lastUpdate": 1749506768059,
+ "triggeredAt": 1749506759851,
"currentValue": "stopped",
"effectiveThreshold": "stopped",
"state": "active",
"escalated": false,
"acknowledged": true,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749383923326,
+ "acknowledgedAt": 1749506768116,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
@@ -1100,7 +964,7 @@
"note": "Bulk acknowledged via dropdown"
},
"compound_1749471240848_s7tobb06w_primary_desktop_100": {
- "id": "alert_1749502225872_hi8xpxptm",
+ "id": "alert_1749506911287_7s6evevo5",
"rule": {
"id": "compound_1749471240848_s7tobb06w",
"name": "Custom Alert",
@@ -1134,10 +998,10 @@
"maxmem": 2147483648,
"maxdisk": 10464022528
},
- "startTime": 1749502225871,
- "lastUpdate": 1749502234638,
+ "startTime": 1749506911287,
+ "lastUpdate": 1749506917939,
"currentValue": {
- "cpu": 9.7
+ "cpu": 4.6
},
"effectiveThreshold": [
{
@@ -1149,10 +1013,10 @@
"state": "resolved",
"escalated": false,
"acknowledged": true,
- "triggeredAt": 1749502232450,
- "resolvedAt": 1749502236824,
+ "triggeredAt": 1749506917939,
+ "resolvedAt": 1749506920164,
"acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749503811808,
+ "acknowledgedAt": 1749506987457,
"acknowledgeNote": "Bulk acknowledged via dropdown",
"note": "Bulk acknowledged via dropdown"
},
@@ -1267,8 +1131,7 @@
"note": "Bulk acknowledged via dropdown"
},
"compound_1749471240848_s7tobb06w_primary_desktop_111": {
- "id": "alert_1749502224406_5r7jkmoga",
- "ruleId": "compound_1749471240848_s7tobb06w",
+ "id": "alert_1749568238371_r7qnu045w",
"rule": {
"id": "compound_1749471240848_s7tobb06w",
"name": "Custom Alert",
@@ -1302,21 +1165,83 @@
"maxmem": 2147483648,
"maxdisk": 4152066048
},
- "severity": "warning",
- "message": "Dynamic threshold rule \"Custom Alert\" triggered for debian: CPU: 10.5% (≥ 4%)",
- "startTime": 1749502224405,
- "lastUpdate": 1749502224405,
- "triggeredAt": 1749502224405,
+ "startTime": 1749568238370,
+ "lastUpdate": 1749568244956,
"currentValue": {
- "cpu": 10.5
+ "cpu": 4
},
+ "effectiveThreshold": [
+ {
+ "metric": "cpu",
+ "condition": "greater_than",
+ "threshold": 4
+ }
+ ],
"state": "resolved",
"escalated": false,
"acknowledged": true,
- "resolvedAt": 1749502225871,
- "acknowledgedBy": "bulk-operation",
- "acknowledgedAt": 1749503811814,
- "acknowledgeNote": "Bulk acknowledged via dropdown",
- "note": "Bulk acknowledged via dropdown"
+ "triggeredAt": 1749568244956,
+ "resolvedAt": 1749568247148,
+ "acknowledgedBy": "web-user",
+ "acknowledgedAt": 1749568251499,
+ "acknowledgeNote": "Acknowledged via web interface",
+ "note": "Acknowledged via web interface"
+ },
+ "compound_1749471240848_s7tobb06w_primary_delly_101": {
+ "id": "alert_1749505855170_5srfusl2c",
+ "rule": {
+ "id": "compound_1749471240848_s7tobb06w",
+ "name": "Custom Alert",
+ "condition": "greater_than",
+ "duration": 5000,
+ "severity": "warning",
+ "enabled": true,
+ "tags": [],
+ "group": "compound_threshold",
+ "escalationTime": 900000,
+ "autoResolve": false,
+ "suppressionTime": 300000,
+ "type": "compound_threshold",
+ "thresholds": [
+ {
+ "metric": "cpu",
+ "condition": "greater_than",
+ "threshold": 4
+ }
+ ],
+ "sendEmail": true,
+ "sendWebhook": false
+ },
+ "guest": {
+ "endpointId": "primary",
+ "node": "delly",
+ "vmid": 101,
+ "name": "homeassistant",
+ "type": "lxc",
+ "status": "running",
+ "maxmem": 2147483648,
+ "maxdisk": 14687412224
+ },
+ "startTime": 1749505855169,
+ "lastUpdate": 1749505863903,
+ "currentValue": {
+ "cpu": 4.4
+ },
+ "effectiveThreshold": [
+ {
+ "metric": "cpu",
+ "condition": "greater_than",
+ "threshold": 4
+ }
+ ],
+ "state": "resolved",
+ "escalated": false,
+ "acknowledged": true,
+ "triggeredAt": 1749505861716,
+ "resolvedAt": 1749505866083,
+ "acknowledgedBy": "web-user",
+ "acknowledgedAt": 1749506285411,
+ "acknowledgeNote": "Acknowledged via web interface",
+ "note": "Acknowledged via web interface"
}
}
\ No newline at end of file
diff --git a/server/alertManager.js b/server/alertManager.js
index 33da9975f..1694a9626 100644
--- a/server/alertManager.js
+++ b/server/alertManager.js
@@ -130,9 +130,9 @@ class AlertManager extends EventEmitter {
const defaultRules = [
{
- id: 'high_cpu',
- name: 'High CPU Usage',
- description: 'Triggers when CPU usage exceeds threshold for specified duration',
+ id: 'cpu',
+ name: 'CPU Usage',
+ description: 'Monitors CPU usage across all VMs and containers',
metric: 'cpu',
condition: 'greater_than',
threshold: cpuThreshold,
@@ -146,25 +146,9 @@ class AlertManager extends EventEmitter {
suppressionTime: 300000, // 5 minutes
},
{
- id: 'critical_cpu',
- name: 'Critical CPU Usage',
- description: 'Critical CPU usage requiring immediate attention',
- metric: 'cpu',
- condition: 'greater_than',
- threshold: this.parseEnvInt('ALERT_CPU_CRITICAL_THRESHOLD', 95, 1, 100),
- duration: this.parseEnvInt('ALERT_CPU_CRITICAL_DURATION', 60000, 1000),
- severity: 'critical',
- enabled: process.env.ALERT_CPU_CRITICAL_ENABLED !== 'false',
- tags: ['performance', 'cpu', 'critical'],
- group: 'critical_alerts',
- escalationTime: 300000, // 5 minutes
- autoResolve: true,
- suppressionTime: 0, // No suppression for critical
- },
- {
- id: 'high_memory',
- name: 'High Memory Usage',
- description: 'Memory usage exceeds safe operating levels',
+ id: 'memory',
+ name: 'Memory Usage',
+ description: 'Monitors memory usage across all VMs and containers',
metric: 'memory',
condition: 'greater_than',
threshold: memThreshold,
@@ -178,25 +162,9 @@ class AlertManager extends EventEmitter {
suppressionTime: 300000,
},
{
- id: 'critical_memory',
- name: 'Critical Memory Usage',
- description: 'Memory usage at critical levels - system stability at risk',
- metric: 'memory',
- condition: 'greater_than',
- threshold: this.parseEnvInt('ALERT_MEMORY_CRITICAL_THRESHOLD', 98, 1, 100),
- duration: this.parseEnvInt('ALERT_MEMORY_CRITICAL_DURATION', 120000, 1000),
- severity: 'critical',
- enabled: process.env.ALERT_MEMORY_CRITICAL_ENABLED !== 'false',
- tags: ['performance', 'memory', 'critical'],
- group: 'critical_alerts',
- escalationTime: 300000, // 5 minutes
- autoResolve: true,
- suppressionTime: 0,
- },
- {
- id: 'disk_space_warning',
- name: 'Low Disk Space',
- description: 'Disk usage approaching capacity limits',
+ id: 'disk',
+ name: 'Disk Usage',
+ description: 'Monitors disk space usage across all VMs and containers',
metric: 'disk',
condition: 'greater_than',
threshold: diskThreshold,
@@ -210,25 +178,9 @@ class AlertManager extends EventEmitter {
suppressionTime: 600000, // 10 minutes
},
{
- id: 'disk_space_critical',
- name: 'Critical Disk Space',
- description: 'Disk space critically low - immediate action required',
- metric: 'disk',
- condition: 'greater_than',
- threshold: this.parseEnvInt('ALERT_DISK_CRITICAL_THRESHOLD', 95, 1, 100),
- duration: this.parseEnvInt('ALERT_DISK_CRITICAL_DURATION', 60000, 1000),
- severity: 'critical',
- enabled: process.env.ALERT_DISK_CRITICAL_ENABLED !== 'false',
- tags: ['storage', 'disk', 'critical'],
- group: 'critical_alerts',
- escalationTime: 300000, // 5 minutes
- autoResolve: true,
- suppressionTime: 0,
- },
- {
- id: 'guest_down',
- name: 'Guest System Down',
- description: 'Virtual machine or container has stopped unexpectedly',
+ id: 'down',
+ name: 'System Availability',
+ description: 'Monitors VM/container availability and uptime',
metric: 'status',
condition: 'equals',
threshold: 'stopped',
@@ -240,29 +192,13 @@ class AlertManager extends EventEmitter {
escalationTime: 600000, // 10 minutes
autoResolve: true,
suppressionTime: 120000, // 2 minutes
- },
- {
- id: 'network_anomaly',
- name: 'Network Traffic Anomaly',
- description: 'Unusual network traffic patterns detected',
- metric: 'network_combined',
- condition: 'anomaly',
- threshold: 'auto', // Machine learning based
- duration: 180000, // 3 minutes
- severity: 'info',
- enabled: process.env.ALERT_NETWORK_ANOMALY_ENABLED === 'true',
- tags: ['network', 'anomaly'],
- group: 'network_alerts',
- escalationTime: 1800000, // 30 minutes
- autoResolve: true,
- suppressionTime: 900000, // 15 minutes
}
];
defaultRules.forEach(rule => {
- if (rule.enabled) {
- this.alertRules.set(rule.id, rule);
- }
+ // Always add rules to the map, regardless of enabled state
+ // This allows them to be displayed and toggled in the UI
+ this.alertRules.set(rule.id, rule);
});
}
@@ -2442,6 +2378,122 @@ This alert was generated by Pulse monitoring system.
this.emailTransporter.close();
}
}
+
+ async sendTestEmail() {
+ try {
+ console.log('[AlertManager] Sending test email...');
+
+ if (!this.emailTransporter) {
+ return { success: false, error: 'Email transporter not configured' };
+ }
+
+ const config = await this.loadEmailConfig();
+ if (!config.to) {
+ return { success: false, error: 'No recipient email address configured' };
+ }
+
+ return await this.sendTestEmailWithConfig({
+ ALERT_FROM_EMAIL: config.from,
+ ALERT_TO_EMAIL: config.to,
+ SMTP_HOST: config.host,
+ SMTP_PORT: config.port,
+ SMTP_USER: config.user,
+ SMTP_SECURE: config.secure
+ });
+
+ } catch (error) {
+ console.error('[AlertManager] Error sending test email:', error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ async sendTestEmailWithConfig(config) {
+ try {
+ console.log('[AlertManager] Sending test email with provided config...');
+
+ if (!this.emailTransporter) {
+ return { success: false, error: 'Email transporter not configured' };
+ }
+
+ if (!config.ALERT_TO_EMAIL) {
+ return { success: false, error: 'No recipient email address configured' };
+ }
+
+ const testEmailOptions = {
+ from: config.ALERT_FROM_EMAIL || 'noreply@pulse.local',
+ to: config.ALERT_TO_EMAIL,
+ subject: 'Pulse Alert System - Test Email',
+ text: `This is a test email from your Pulse monitoring system.
+
+Sent at: ${new Date().toISOString()}
+From: ${require('os').hostname()}
+
+If you received this email, your email configuration is working correctly!
+
+Configuration used:
+- SMTP Host: ${config.SMTP_HOST}
+- SMTP Port: ${config.SMTP_PORT}
+- From: ${config.ALERT_FROM_EMAIL}
+- To: ${config.ALERT_TO_EMAIL}
+
+Best regards,
+Pulse Monitoring System`,
+ html: `
+
Pulse Alert System - Test Email
+
This is a test email from your Pulse monitoring system.
+
+
+ Test Details:
+ Sent at: ${new Date().toISOString()}
+ From: ${require('os').hostname()}
+ Configuration used:
+ SMTP Host: ${config.SMTP_HOST}
+ SMTP Port: ${config.SMTP_PORT}
+ From: ${config.ALERT_FROM_EMAIL}
+ To: ${config.ALERT_TO_EMAIL}
+
+
+
✅ Success! If you received this email, your email configuration is working correctly!
+
+
+
+ Best regards,
+ Pulse Monitoring System
+
+
`
+ };
+
+ await this.emailTransporter.sendMail(testEmailOptions);
+ console.log('[AlertManager] Test email sent successfully');
+ return { success: true };
+
+ } catch (error) {
+ console.error('[AlertManager] Error sending test email:', error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ async loadEmailConfig() {
+ try {
+ // Load email configuration from environment or config
+ const { loadConfiguration } = require('./configLoader');
+ const config = await loadConfiguration();
+
+ console.log('[AlertManager] Loading email config, ALERT_TO_EMAIL:', config.ALERT_TO_EMAIL);
+
+ return {
+ from: config.ALERT_FROM_EMAIL,
+ to: config.ALERT_TO_EMAIL,
+ host: config.SMTP_HOST,
+ port: config.SMTP_PORT,
+ user: config.SMTP_USER,
+ secure: config.SMTP_SECURE === 'true'
+ };
+ } catch (error) {
+ console.error('[AlertManager] Error loading email config:', error);
+ return {};
+ }
+ }
}
module.exports = AlertManager;
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 7c95160fd..27db50651 100644
--- a/server/index.js
+++ b/server/index.js
@@ -466,6 +466,47 @@ app.get('/api/alerts/metrics', (req, res) => {
}
});
+// Test email configuration
+app.post('/api/alerts/test-email', async (req, res) => {
+ try {
+ console.log('[Test Email] Sending test email...');
+
+ // Get email configuration from process.env (loaded by configLoader)
+ const config = {
+ ALERT_TO_EMAIL: process.env.ALERT_TO_EMAIL,
+ ALERT_FROM_EMAIL: process.env.ALERT_FROM_EMAIL,
+ SMTP_HOST: process.env.SMTP_HOST,
+ SMTP_PORT: process.env.SMTP_PORT,
+ SMTP_USER: process.env.SMTP_USER,
+ SMTP_SECURE: process.env.SMTP_SECURE
+ };
+
+ console.log('[Test Email] Config loaded, ALERT_TO_EMAIL:', config.ALERT_TO_EMAIL);
+
+ if (!config.ALERT_TO_EMAIL) {
+ return res.status(400).json({ success: false, error: 'No recipient email address configured' });
+ }
+
+ if (!config.SMTP_HOST) {
+ return res.status(400).json({ success: false, error: 'SMTP server not configured' });
+ }
+
+ // Use the alert manager to send a test email with the config
+ const testResult = await stateManager.alertManager.sendTestEmailWithConfig(config);
+
+ if (testResult.success) {
+ console.log('[Test Email] Test email sent successfully');
+ res.json({ success: true, message: 'Test email sent successfully' });
+ } else {
+ console.error('[Test Email] Failed to send test email:', testResult.error);
+ res.status(400).json({ success: false, error: testResult.error || 'Failed to send test email' });
+ }
+ } catch (error) {
+ console.error('[Test Email] Error sending test email:', error);
+ res.status(500).json({ success: false, error: 'Internal server error while sending test email' });
+ }
+});
+
// Alert rules management with filtering
app.get('/api/alerts/rules', (req, res) => {
try {
diff --git a/src/public/js/ui/alertManagementModal.js b/src/public/js/ui/alertManagementModal.js
index 3bb08c1bf..c52682263 100644
--- a/src/public/js/ui/alertManagementModal.js
+++ b/src/public/js/ui/alertManagementModal.js
@@ -5,6 +5,8 @@ PulseApp.ui.alertManagementModal = (() => {
let activeTab = 'alerts';
let currentConfig = {};
let formDataCache = {};
+ let isLoading = false;
+ let refreshInterval = null;
function init() {
if (isInitialized) return;
@@ -15,75 +17,164 @@ PulseApp.ui.alertManagementModal = (() => {
// Set up event listeners
setupEventListeners();
+ // Expose global functions after they are defined
+ exposeGlobalFunctions();
+
// Set up real-time updates for alert management modal
if (window.socket) {
// Auto-refresh when alerts change
window.socket.on('alert', () => {
- if (isModalOpen()) {
- loadCurrentAlerts();
- // Update alert rules to refresh count badges
- loadSystemAlerts();
- loadCustomAlerts();
+ try {
+ if (isModalOpen()) {
+ coordinatedRefresh();
+ }
+ } catch (error) {
+ console.error('[Alert Modal] Error handling alert event:', error);
}
});
window.socket.on('alertResolved', () => {
- if (isModalOpen()) {
- loadCurrentAlerts();
- // Update alert rules to refresh count badges
- loadSystemAlerts();
- loadCustomAlerts();
+ try {
+ if (isModalOpen()) {
+ coordinatedRefresh();
+ }
+ } catch (error) {
+ console.error('[Alert Modal] Error handling alertResolved event:', error);
}
});
window.socket.on('alertAcknowledged', () => {
- if (isModalOpen()) {
- loadCurrentAlerts();
- // Update alert rules to refresh count badges
- loadSystemAlerts();
- loadCustomAlerts();
+ try {
+ if (isModalOpen()) {
+ coordinatedRefresh();
+ }
+ } catch (error) {
+ console.error('[Alert Modal] Error handling alertAcknowledged event:', error);
}
});
window.socket.on('alertEscalated', () => {
- if (isModalOpen()) {
- loadCurrentAlerts();
- // Update alert rules to refresh count badges
- loadSystemAlerts();
- loadCustomAlerts();
+ try {
+ if (isModalOpen()) {
+ coordinatedRefresh();
+ }
+ } catch (error) {
+ console.error('[Alert Modal] Error handling alertEscalated event:', error);
}
});
}
// Set up periodic refresh to update durations every 2 seconds (for near real-time updates)
- setInterval(() => {
+ refreshInterval = setInterval(() => {
if (isModalOpen()) {
- loadCurrentAlerts();
- // Also refresh alert rules to update count badges
- loadSystemAlerts();
- loadCustomAlerts();
+ coordinatedRefresh();
}
}, 2000);
isInitialized = true;
}
+
+ function filterAlertsBySeverity(severity) {
+ // Switch to monitor tab to show alerts
+ switchTab('monitor');
+
+ // Clear any existing search since we no longer filter by severity
+ const searchInput = document.getElementById('alert-search');
+ if (searchInput) {
+ searchInput.value = '';
+ searchInput.dispatchEvent(new Event('input'));
+ }
+ }
+
+ function exposeGlobalFunctions() {
+ // Make all onclick handler functions globally accessible immediately
+ // This prevents timing issues where HTML is rendered before functions are exposed
+ const functions = {
+ deleteCustomAlert,
+ openCustomAlertModal,
+ showAlertDetails,
+ toggleAlert,
+ addWebhookEndpoint,
+ removeWebhookEndpoint,
+ handleEmailProviderSelection,
+ filterAlertsBySeverity,
+ switchTab,
+ // Unified alert rule functions
+ openAlertRuleModal,
+ editAlertRule,
+ toggleAlertRule,
+ deleteAlertRule
+ };
+
+ Object.entries(functions).forEach(([name, func]) => {
+ window[name] = func;
+ });
+ }
+
+ function coordinatedRefresh() {
+ // Prevent concurrent refreshes
+ if (isLoading) return;
+
+ isLoading = true;
+ try {
+ loadCurrentAlerts();
+ loadSystemAlerts();
+ loadCustomAlerts();
+ } catch (error) {
+ console.error('[Alert Modal] Error during coordinated refresh:', error);
+ } finally {
+ isLoading = false;
+ }
+ }
+
function openModal() {
const modal = document.getElementById('alert-management-modal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
- // Default to alerts tab when opening
- switchTab('alerts');
+ // Default to monitor tab when opening
+ switchTab('monitor');
+
+ // Restart the refresh interval if it was cleaned up
+ if (!refreshInterval) {
+ refreshInterval = setInterval(() => {
+ if (isModalOpen()) {
+ coordinatedRefresh();
+ }
+ }, 2000);
+ }
}
}
+ function cleanup() {
+ // Clear the refresh interval to prevent memory leaks
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ refreshInterval = null;
+ }
+
+ // Remove socket event listeners to prevent memory leaks
+ if (window.socket) {
+ window.socket.off('alert');
+ window.socket.off('alertResolved');
+ window.socket.off('alertAcknowledged');
+ window.socket.off('alertEscalated');
+ }
+
+ // Reset state
+ isLoading = false;
+ }
+
function closeModal() {
const modal = document.getElementById('alert-management-modal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
+
+ // Clean up resources to prevent memory leaks
+ cleanup();
}
function createModalHTML() {
@@ -107,14 +198,19 @@ PulseApp.ui.alertManagementModal = (() => {
@@ -128,9 +224,6 @@ PulseApp.ui.alertManagementModal = (() => {
Close
-
- Save Changes
-
@@ -144,7 +237,6 @@ PulseApp.ui.alertManagementModal = (() => {
const modal = document.getElementById('alert-management-modal');
const closeButton = document.getElementById('alert-management-modal-close');
const cancelButton = document.getElementById('alert-management-cancel-button');
- const saveButton = document.getElementById('alert-management-save-button');
if (closeButton) {
closeButton.addEventListener('click', closeModal);
@@ -154,10 +246,6 @@ PulseApp.ui.alertManagementModal = (() => {
cancelButton.addEventListener('click', closeModal);
}
- if (saveButton) {
- saveButton.addEventListener('click', saveConfiguration);
- }
-
// Close modal when clicking outside
if (modal) {
modal.addEventListener('click', (e) => {
@@ -211,16 +299,6 @@ PulseApp.ui.alertManagementModal = (() => {
}
});
- // Show/hide the save button based on the active tab
- // Only show save button on notifications tab where there might be unsaved changes
- const saveButton = document.getElementById('alert-management-save-button');
- if (saveButton) {
- if (tabName === 'notifications') {
- saveButton.style.display = 'block';
- } else {
- saveButton.style.display = 'none';
- }
- }
// Update content
renderTabContent();
@@ -238,25 +316,26 @@ PulseApp.ui.alertManagementModal = (() => {
if (!modalBody) return;
switch (activeTab) {
- case 'alerts':
- modalBody.innerHTML = renderAlertsTab();
- initializeAlertsTab();
+ case 'monitor':
+ modalBody.innerHTML = renderMonitorTab();
+ initializeMonitorTab();
break;
- case 'alert-rules':
- modalBody.innerHTML = renderAlertRulesTab();
- initializeAlertRulesTab();
- break;
- case 'notifications':
- modalBody.innerHTML = renderNotificationsTab();
- initializeNotificationsTab();
- // Ensure configuration is loaded before populating email fields
+ case 'configure':
+ modalBody.innerHTML = renderConfigureTab();
+ initializeConfigureTab();
+ // Load configuration for the configure tab
if (currentConfig && Object.keys(currentConfig).length > 0) {
- // Config already loaded, populate immediately
- setTimeout(() => loadEmailConfiguration(), 50);
- } else {
- // Config not loaded yet, wait for it
- loadConfiguration().then(() => {
+ setTimeout(async () => {
loadEmailConfiguration();
+ await loadGlobalToggles();
+ }, 50);
+ } else {
+ loadConfiguration().then(async () => {
+ // Add a small delay to ensure DOM is ready
+ setTimeout(async () => {
+ loadEmailConfiguration();
+ await loadGlobalToggles();
+ }, 100);
});
}
break;
@@ -265,6 +344,1012 @@ PulseApp.ui.alertManagementModal = (() => {
}
}
+ function renderMonitorTab() {
+ return `
+
+
+
+
+
+
+
+
+
+
Active Alerts
+
0
+
+
+
+
+ Refresh
+
+
+
+ Acknowledge All
+
+
+
+
+
Loading active alerts...
+
+
+
+
+
+
+
+
+
Recent Activity
+
(Last 24 hours)
+
+
+
+
Loading recent activity...
+
+
+
+ `;
+ }
+
+ function renderConfigureTab() {
+ return `
+
+
+
+
+
+
+
+
+
+ Create Rule
+
+
+
+
+
+
+
+
+
Loading alert rules...
+
+
+
+
+
+
+
+
+
+
Notification Settings
+
+
+
+
+
+
+
+
Global Notification Controls
+
Master switches for all alert notifications
+
+
+
+
+
+
+
+
+
+
+
Email Configuration
+
+
+
+
+
+
+
+ Test Email
+
+
+ Save Configuration
+
+
+
+
+
+
+
+
Webhook Configuration
+
+
+
+
+
+
+
+
+ Test Webhook
+
+
+ Save Configuration
+
+
+
+
+
+
+
+ `;
+ }
+
+ function initializeMonitorTab() {
+ // Load current alerts and populate the monitor tab
+ loadCurrentAlerts();
+ loadRecentActivity();
+ updateAlertSummary();
+
+ // Set up button event listeners
+ const refreshBtn = document.getElementById('refresh-alerts-btn');
+ if (refreshBtn) {
+ refreshBtn.addEventListener('click', () => {
+ coordinatedRefresh();
+ loadRecentActivity();
+ updateAlertSummary();
+ });
+ }
+
+ const acknowledgeAllBtn = document.getElementById('acknowledge-all-alerts-btn');
+ if (acknowledgeAllBtn) {
+ acknowledgeAllBtn.addEventListener('click', () => {
+ if (PulseApp.alerts && PulseApp.alerts.markAllAsAcknowledged) {
+ PulseApp.alerts.markAllAsAcknowledged();
+ }
+ });
+ }
+ }
+
+ function initializeConfigureTab() {
+ // Load all alert rules (unified)
+ loadAllAlertRules();
+
+ // Set up event listeners for notifications
+ setupNotificationToggles();
+ setupEmailTestButton();
+
+ // Create rule button now uses onclick attribute directly
+ }
+
+ async function loadAllAlertRules() {
+ const alertRulesList = document.getElementById('alert-rules-list');
+ if (!alertRulesList) return;
+
+ try {
+ // Load all alert rules from the API
+ const response = await fetch('/api/alerts/rules');
+ const data = await response.json();
+ const rules = data.rules || [];
+
+ if (rules.length === 0) {
+ alertRulesList.innerHTML = `
+
+
+
No alert rules configured
+
+ Create First Rule
+
+
+ `;
+ return;
+ }
+
+ // Sort rules by type (built-in first) then by name
+ rules.sort((a, b) => {
+ if (a.isBuiltIn && !b.isBuiltIn) return -1;
+ if (!a.isBuiltIn && b.isBuiltIn) return 1;
+ return (a.name || '').localeCompare(b.name || '');
+ });
+
+ alertRulesList.innerHTML = rules.map(rule => renderUnifiedAlertRule(rule)).join('');
+
+ } catch (error) {
+ console.error('Error loading alert rules:', error);
+ alertRulesList.innerHTML = `
+
+
Error loading alert rules
+
+ `;
+ }
+ }
+
+ function renderUnifiedAlertRule(rule) {
+ const isBuiltIn = rule.isBuiltIn || rule.type === 'built-in' ||
+ ['cpu', 'memory', 'disk', 'down'].includes(rule.id);
+
+ // Generate threshold display
+ const thresholdDisplay = (() => {
+ if (rule.thresholds && Array.isArray(rule.thresholds)) {
+ // Custom compound rule
+ const thresholds = rule.thresholds.map(t => {
+ const unit = ['cpu', 'memory', 'disk'].includes(t.metric) ? '%' : '';
+ return `${t.metric.toUpperCase()} ${t.condition === 'greater_than' ? '>' : t.condition === 'less_than' ? '<' : '≥'} ${t.threshold}${unit}`;
+ });
+ return thresholds.join(' AND ');
+ } else if (rule.threshold !== undefined) {
+ // Simple rule with single threshold
+ const unit = ['cpu', 'memory', 'disk'].includes(rule.metric) ? '%' : '';
+ const condition = rule.condition === 'greater_than' ? '>' : rule.condition === 'less_than' ? '<' : '≥';
+ return `${rule.metric ? rule.metric.toUpperCase() : 'VALUE'} ${condition} ${rule.threshold}${unit}`;
+ }
+ return '';
+ })();
+
+ // Generate duration display
+ const durationDisplay = (() => {
+ if (rule.duration) {
+ const minutes = Math.round(rule.duration / 60000);
+ return minutes === 1 ? '1 min' : `${minutes} mins`;
+ }
+ return '';
+ })();
+
+ return `
+
+
+
+
${rule.name || rule.id}
+ ${isBuiltIn ?
+ 'Built-in' :
+ 'Custom'
+ }
+
+
+ ${thresholdDisplay ? `
+
+ Condition:
+ ${thresholdDisplay}
+ ${durationDisplay ? `for ${durationDisplay}` : ''}
+
+ ` : ''}
+
${rule.description || getAlertRuleDescription(rule)}
+
+
+
+
+
+
+
+ ${!isBuiltIn ? `
+
+
+
+ ` : ''}
+
+
+ `;
+ }
+
+ function getAlertRuleDescription(rule) {
+ if (rule.description) return rule.description;
+
+ // Generate description for built-in rules
+ switch (rule.id) {
+ case 'cpu':
+ return 'Monitors CPU usage across all VMs and containers';
+ case 'memory':
+ return 'Monitors memory usage across all VMs and containers';
+ case 'disk':
+ return 'Monitors disk space usage across all VMs and containers';
+ case 'down':
+ return 'Monitors VM/container availability and uptime';
+ default:
+ if (rule.thresholds && rule.thresholds.length > 0) {
+ return `Custom rule with ${rule.thresholds.length} threshold${rule.thresholds.length > 1 ? 's' : ''}`;
+ }
+ return 'Custom alert rule';
+ }
+ }
+
+ // Unified alert rule functions
+ function openAlertRuleModal(existingRule = null) {
+ // For now, redirect to the existing custom alert modal
+ // TODO: Create a unified modal that handles both built-in and custom rules
+ openCustomAlertModal([], existingRule);
+ }
+
+ async function editAlertRule(ruleId) {
+ try {
+ // Fetch the current alert rule data from API
+ const response = await fetch('/api/alerts/rules');
+ if (!response.ok) {
+ throw new Error(`Failed to fetch alert rules: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const rule = data.rules ? data.rules.find(r => r.id === ruleId) : null;
+
+ if (!rule) {
+ throw new Error('Alert rule not found');
+ }
+
+ // Convert built-in rules to compound threshold format for unified editing
+ let thresholds = [];
+ let alertData = rule;
+
+ if (['cpu', 'memory', 'disk', 'down'].includes(ruleId)) {
+ // Convert simple built-in rule to compound threshold format
+ if (rule.metric && rule.threshold !== undefined) {
+ thresholds = [{
+ metric: rule.metric,
+ condition: rule.condition || 'greater_than',
+ threshold: rule.threshold
+ }];
+ }
+
+ // Create alertData object that openCustomAlertModal can understand
+ alertData = {
+ ...rule,
+ type: 'compound_threshold',
+ thresholds: thresholds
+ };
+ } else {
+ // Custom rule - use existing thresholds
+ thresholds = rule.thresholds || [];
+ }
+
+ // Open the unified custom alert modal with pre-filled data
+ openCustomAlertModal(thresholds, alertData);
+
+ } catch (error) {
+ console.error('Failed to load alert rule for editing:', error);
+ PulseApp.ui.toast.error('Failed to load alert rule for editing');
+ }
+ }
+
+ async function toggleAlertRule(ruleId, enabled) {
+ // Check if it's a built-in rule or custom rule
+ const isBuiltIn = ['cpu', 'memory', 'disk', 'down'].includes(ruleId);
+
+ if (isBuiltIn) {
+ return toggleSystemAlert(ruleId, enabled);
+ } else {
+ return toggleCustomAlert(ruleId, enabled);
+ }
+ }
+
+ async function deleteAlertRule(ruleId) {
+ if (confirm('Are you sure you want to delete this alert rule? This action cannot be undone.')) {
+ try {
+ const response = await fetch(`/api/alerts/rules/${ruleId}`, {
+ method: 'DELETE'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || `Server error: ${response.status}`);
+ }
+
+ PulseApp.ui.toast.success('Alert rule deleted successfully');
+ await loadAllAlertRules(); // Refresh the list
+
+ } catch (error) {
+ console.error('Failed to delete alert rule:', error);
+ PulseApp.ui.toast.error('Failed to delete alert rule');
+ }
+ }
+ }
+
+ function updateAlertSummary() {
+ // Get current alerts and update summary cards
+ if (PulseApp.alerts && PulseApp.alerts.getActiveAlerts) {
+ const alerts = PulseApp.alerts.getActiveAlerts();
+ const acknowledgedAlerts = alerts.filter(alert => alert.acknowledged);
+ const activeAlerts = alerts.filter(alert => !alert.acknowledged);
+
+ // Update the count displays with null checks
+ const activeAlertsEl = document.getElementById('active-alerts-count');
+ const acknowledgedAlertsEl = document.getElementById('acknowledged-alerts-count');
+ const badgeEl = document.getElementById('active-alerts-badge');
+
+ if (activeAlertsEl) activeAlertsEl.textContent = activeAlerts.length;
+ if (acknowledgedAlertsEl) acknowledgedAlertsEl.textContent = acknowledgedAlerts.length;
+ if (badgeEl) badgeEl.textContent = activeAlerts.length; // Badge shows only active alerts
+
+ console.log(`[Alert Summary] Updated counts: Active=${activeAlerts.length}, Acknowledged=${acknowledgedAlerts.length}, Total=${alerts.length}`);
+ } else {
+ console.log('[Alert Summary] PulseApp.alerts not available or getCurrentAlerts not found');
+ }
+
+ // Update rules count
+ updateRulesCount();
+ }
+
+ async function updateRulesCount() {
+ try {
+ const response = await fetch('/api/alerts/rules');
+ if (response.ok) {
+ const data = await response.json();
+ const rulesCount = data.rules ? data.rules.length : 0;
+ const totalRulesEl = document.getElementById('total-rules-count');
+ if (totalRulesEl) totalRulesEl.textContent = rulesCount;
+ }
+ } catch (error) {
+ console.error('Failed to load rules count:', error);
+ }
+ }
+
+ async function loadRecentActivity() {
+ const activityList = document.getElementById('recent-activity-list');
+ if (!activityList) return;
+
+ try {
+ // Load recent alert history (last 24 hours)
+ const response = await fetch('/api/alerts/history?limit=20');
+ if (!response.ok) throw new Error('Failed to fetch recent activity');
+
+ const data = await response.json();
+ const recentAlerts = data.history || [];
+
+ // Filter to last 24 hours
+ const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
+ const recent = recentAlerts.filter(alert =>
+ (alert.triggeredAt || alert.resolvedAt || alert.acknowledgedAt) >= oneDayAgo
+ );
+
+ if (recent.length === 0) {
+ activityList.innerHTML = `
+
+
+
No recent activity
+
This is a good thing!
+
+ `;
+ return;
+ }
+
+ // Render recent activity items
+ const content = recent.map(alert => renderActivityItem(alert)).join('');
+ activityList.innerHTML = content;
+
+ } catch (error) {
+ console.error('Error loading recent activity:', error);
+ activityList.innerHTML = `
+
+
+
Error loading recent activity
+
+ `;
+ }
+ }
+
+ function renderActivityItem(alert) {
+ const time = new Date(alert.triggeredAt || alert.resolvedAt || alert.acknowledgedAt);
+ const timeAgo = getTimeAgo(time);
+ const severityColor = getSeverityColor(alert.severity);
+
+ let statusIcon = '';
+ let statusText = '';
+
+ if (alert.resolvedAt) {
+ statusIcon = '';
+ statusText = 'Resolved';
+ } else if (alert.acknowledgedAt) {
+ statusIcon = '';
+ statusText = 'Acknowledged';
+ } else {
+ statusIcon = '';
+ statusText = 'Triggered';
+ }
+
+ return `
+
+
+
+ ${statusIcon}
+
+
+ ${alert.ruleName || 'Unknown Rule'}
+
+
+ ${alert.guest?.name || 'Unknown'} on ${alert.guest?.node || 'Unknown'}
+
+
+
+
+
+ ${(alert.severity || 'warning').charAt(0).toUpperCase() + (alert.severity || 'warning').slice(1)}
+
+ ${timeAgo}
+
+
+
+ `;
+ }
+
+ function getSeverityColor(severity) {
+ const colors = {
+ critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
+ warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
+ };
+ return colors[severity] || colors.warning;
+ }
+
+ function getTimeAgo(date) {
+ const now = new Date();
+ const diffMs = now - date;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ return `${diffDays}d ago`;
+ }
+
+ function loadSystemRules() {
+ const systemRulesList = document.getElementById('system-rules-list');
+ if (!systemRulesList) return;
+
+ renderSystemRulesForConfigure();
+ }
+
+ function loadCustomRules() {
+ const customRulesList = document.getElementById('custom-rules-list');
+ if (!customRulesList) return;
+
+ renderCustomRulesForConfigure();
+ }
+
+ function renderSystemRulesForConfigure() {
+ const systemRulesList = document.getElementById('system-rules-list');
+ if (!systemRulesList) return;
+
+ // System rules with current state from environment variables
+ const systemRules = [
+ {
+ id: 'cpu',
+ name: 'CPU Usage',
+ description: 'Monitor CPU usage across VMs/LXCs',
+ enabled: currentConfig?.ALERT_CPU_ENABLED !== 'false'
+ },
+ {
+ id: 'memory',
+ name: 'Memory Usage',
+ description: 'Monitor memory usage across VMs/LXCs',
+ enabled: currentConfig?.ALERT_MEMORY_ENABLED !== 'false'
+ },
+ {
+ id: 'disk',
+ name: 'Disk Usage',
+ description: 'Monitor disk space across VMs/LXCs',
+ enabled: currentConfig?.ALERT_DISK_ENABLED === 'true'
+ },
+ {
+ id: 'down',
+ name: 'VM/LXC Down',
+ description: 'Alert when VMs or LXCs go offline',
+ enabled: currentConfig?.ALERT_DOWN_ENABLED === 'true'
+ }
+ ];
+
+ const rulesHTML = systemRules.map(rule => `
+
+
+
+
+
${rule.name}
+
${rule.description}
+
+
+
+
+ `).join('');
+
+ systemRulesList.innerHTML = rulesHTML;
+ }
+
+ function renderCustomRulesForConfigure() {
+ const customRulesList = document.getElementById('custom-rules-list');
+ if (!customRulesList) return;
+
+ // Get custom alert rules from the existing system
+ fetch('/api/alerts/rules?group=custom')
+ .then(response => response.json())
+ .then(data => {
+ const customRules = data.rules || [];
+
+ if (customRules.length === 0) {
+ customRulesList.innerHTML = `
+
+
+
No custom rules yet
+
+ Create your first custom rule
+
+
+ `;
+ return;
+ }
+
+ const rulesHTML = customRules.map(rule => `
+
+
+
+
+
${rule.name || 'Unnamed Rule'}
+
${rule.description || 'No description'}
+
+
+ ${(rule.severity || 'warning').charAt(0).toUpperCase() + (rule.severity || 'warning').slice(1)}
+
+ ${getActiveAlertsCount(rule.id) > 0 ? `${getActiveAlertsCount(rule.id)} active` : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `).join('');
+
+ customRulesList.innerHTML = rulesHTML;
+ })
+ .catch(error => {
+ console.error('Error loading custom rules:', error);
+ customRulesList.innerHTML = `
+
+
+
Error loading custom rules
+
+ `;
+ });
+ }
+
+ function getSeverityBadgeColor(severity) {
+ const colors = {
+ critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
+ warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
+ };
+ return colors[severity] || colors.warning;
+ }
+
+ function getActiveAlertsCount(ruleId) {
+ if (PulseApp.alerts && PulseApp.alerts.getCurrentAlerts) {
+ const alerts = PulseApp.alerts.getCurrentAlerts();
+ return alerts.filter(alert => alert.ruleId === ruleId).length;
+ }
+ return 0;
+ }
+
+ function setupNotificationToggles() {
+ const emailToggle = document.getElementById('global-email-toggle');
+ const webhookToggle = document.getElementById('global-webhook-toggle');
+
+ if (emailToggle) {
+ emailToggle.addEventListener('change', (e) => {
+ handleGlobalEmailToggle(e.target.checked);
+ });
+ }
+
+ if (webhookToggle) {
+ webhookToggle.addEventListener('change', (e) => {
+ handleGlobalWebhookToggle(e.target.checked);
+ });
+ }
+ }
+
+ function setupEmailTestButton() {
+ const testBtn = document.getElementById('test-email-btn');
+ if (testBtn) {
+ testBtn.addEventListener('click', testEmailConfiguration);
+ }
+ }
+
+ async function testEmailConfiguration() {
+ const testBtn = document.getElementById('test-email-btn');
+ const originalText = testBtn.textContent;
+
+ try {
+ testBtn.disabled = true;
+ testBtn.textContent = 'Testing...';
+
+ const response = await fetch('/api/alerts/test-email', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ testBtn.textContent = 'Test Sent!';
+ testBtn.classList.remove('bg-gray-200', 'hover:bg-gray-300');
+ testBtn.classList.add('bg-green-500', 'text-white');
+ PulseApp.ui.toast.success('Test email sent successfully! Check your inbox.');
+ } else {
+ testBtn.textContent = 'Test Failed';
+ testBtn.classList.remove('bg-gray-200', 'hover:bg-gray-300');
+ testBtn.classList.add('bg-red-500', 'text-white');
+ PulseApp.ui.toast.error(result.error || 'Failed to send test email');
+ }
+
+ // Reset button after 3 seconds
+ setTimeout(() => {
+ testBtn.textContent = originalText;
+ testBtn.classList.remove('bg-green-500', 'bg-red-500', 'text-white');
+ testBtn.classList.add('bg-gray-200', 'hover:bg-gray-300');
+ testBtn.disabled = false;
+ }, 3000);
+
+ } catch (error) {
+ console.error('[Test Email] Error:', error);
+ testBtn.textContent = originalText;
+ testBtn.disabled = false;
+ PulseApp.ui.toast.error('Failed to test email configuration');
+ }
+ }
+
+ function handleGlobalEmailToggle(enabled) {
+ console.log(`[Email Toggle] Setting GLOBAL_EMAIL_ENABLED to: ${enabled}`);
+
+ // Save the setting
+ fetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ GLOBAL_EMAIL_ENABLED: enabled ? 'true' : 'false' })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ console.log(`[Email Toggle] Successfully ${enabled ? 'enabled' : 'disabled'} global email notifications`);
+ PulseApp.ui.toast.success(`Email notifications ${enabled ? 'enabled' : 'disabled'}`);
+ updateEmailConfigVisibility(enabled);
+ // Update currentConfig immediately
+ if (currentConfig) {
+ currentConfig.GLOBAL_EMAIL_ENABLED = enabled ? 'true' : 'false';
+ }
+ } else {
+ console.error('[Email Toggle] Failed to save setting');
+ PulseApp.ui.toast.error('Failed to save email setting');
+ }
+ })
+ .catch(error => {
+ console.error('[Email Toggle] Error saving setting:', error);
+ PulseApp.ui.toast.error('Failed to save email setting');
+ });
+ }
+
+ function handleGlobalWebhookToggle(enabled) {
+ console.log(`[Webhook Toggle] Setting GLOBAL_WEBHOOK_ENABLED to: ${enabled}`);
+
+ // Save the setting
+ fetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ GLOBAL_WEBHOOK_ENABLED: enabled ? 'true' : 'false' })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ console.log(`[Webhook Toggle] Successfully ${enabled ? 'enabled' : 'disabled'} global webhook notifications`);
+ PulseApp.ui.toast.success(`Webhook notifications ${enabled ? 'enabled' : 'disabled'}`);
+ updateWebhookConfigVisibility(enabled);
+ // Update currentConfig immediately
+ if (currentConfig) {
+ currentConfig.GLOBAL_WEBHOOK_ENABLED = enabled ? 'true' : 'false';
+ }
+ } else {
+ console.error('[Webhook Toggle] Failed to save setting');
+ PulseApp.ui.toast.error('Failed to save webhook setting');
+ }
+ })
+ .catch(error => {
+ console.error('[Webhook Toggle] Error saving setting:', error);
+ PulseApp.ui.toast.error('Failed to save webhook setting');
+ });
+ }
+
+ function updateEmailConfigVisibility(enabled) {
+ const emailSection = document.getElementById('email-config-section');
+ if (emailSection) {
+ emailSection.style.opacity = enabled ? '1' : '0.5';
+ // Only disable input fields, not test/save buttons
+ const inputs = emailSection.querySelectorAll('input[name*="SMTP"], input[name*="ALERT_"]');
+ inputs.forEach(input => {
+ input.disabled = !enabled;
+ });
+ }
+ }
+
+ function updateWebhookConfigVisibility(enabled) {
+ const webhookSection = document.getElementById('webhook-config-section');
+ if (webhookSection) {
+ webhookSection.style.opacity = enabled ? '1' : '0.5';
+ // Only disable input fields, not test/save buttons
+ const inputs = webhookSection.querySelectorAll('input[name*="WEBHOOK"]');
+ inputs.forEach(input => {
+ input.disabled = !enabled;
+ });
+ }
+ }
+
+ async function loadGlobalToggles() {
+ // Load and set the current state of global toggles
+ const emailToggle = document.getElementById('global-email-toggle');
+ const webhookToggle = document.getElementById('global-webhook-toggle');
+
+ // Ensure we have fresh config
+ if (!currentConfig || Object.keys(currentConfig).length === 0) {
+ await loadConfiguration();
+ }
+
+ console.log('[Global Toggles] Loading with config:', {
+ GLOBAL_EMAIL_ENABLED: currentConfig.GLOBAL_EMAIL_ENABLED,
+ GLOBAL_WEBHOOK_ENABLED: currentConfig.GLOBAL_WEBHOOK_ENABLED
+ });
+
+ if (emailToggle && currentConfig) {
+ const emailEnabled = currentConfig.GLOBAL_EMAIL_ENABLED === 'true';
+ console.log('[Global Toggles] Setting email toggle to:', emailEnabled);
+ emailToggle.checked = emailEnabled;
+ updateEmailConfigVisibility(emailEnabled);
+ }
+
+ if (webhookToggle && currentConfig) {
+ const webhookEnabled = currentConfig.GLOBAL_WEBHOOK_ENABLED === 'true';
+ console.log('[Global Toggles] Setting webhook toggle to:', webhookEnabled);
+ webhookToggle.checked = webhookEnabled;
+ updateWebhookConfigVisibility(webhookEnabled);
+ }
+ }
+
function renderAlertsTab() {
return `
@@ -573,24 +1658,24 @@ PulseApp.ui.alertManagementModal = (() => {
-
+
-
+
@@ -623,7 +1708,7 @@ PulseApp.ui.alertManagementModal = (() => {
-
+
@@ -747,7 +1832,7 @@ PulseApp.ui.alertManagementModal = (() => {
acknowledgeAllBtn.addEventListener('click', () => {
if (PulseApp.alerts && PulseApp.alerts.markAllAsAcknowledged) {
PulseApp.alerts.markAllAsAcknowledged();
- setTimeout(loadCurrentAlerts, 500); // Refresh after acknowledging
+ // Socket events will handle the real-time updates
}
});
}
@@ -896,7 +1981,7 @@ PulseApp.ui.alertManagementModal = (() => {
autoDetectEmailProvider(emailFromInput.value);
// Auto-fill username with email address for common providers
- const usernameInput = document.querySelector('input[name="ALERT_SMTP_USER"]');
+ const usernameInput = document.querySelector('input[name="SMTP_USER"]');
if (usernameInput && emailFromInput.value && emailFromInput.value.includes('@')) {
const domain = emailFromInput.value.split('@')[1].toLowerCase();
const commonProviders = ['gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com', 'live.com', 'msn.com', 'yahoo.com', 'yahoo.co.uk', 'yahoo.ca', 'ymail.com'];
@@ -1401,94 +2486,108 @@ PulseApp.ui.alertManagementModal = (() => {
}
function loadEmailConfiguration() {
- const emailConfigSection = document.getElementById('primary-email-config');
- if (!emailConfigSection) return;
+ console.log('[Email Config] loadEmailConfiguration called');
+
+ // Check if we're in the configure tab (different structure)
+ const isConfigureTab = document.getElementById('email-config-section');
+ if (!isConfigureTab) {
+ console.log('[Email Config] Not in configure tab, looking for primary-email-config');
+ const emailConfigSection = document.getElementById('primary-email-config');
+ if (!emailConfigSection) {
+ console.log('[Email Config] primary-email-config not found either, returning');
+ return;
+ }
+ } else {
+ console.log('[Email Config] In configure tab');
+ }
// Get email configuration from currentConfig
const config = currentConfig || {};
const smtp = config.advanced?.smtp || {};
+ console.log('[Email Config] Loading email configuration:', smtp);
+ console.log('[Email Config] Full config:', config);
+ console.log('[Email Config] Direct config keys:', {
+ SMTP_HOST: config.SMTP_HOST,
+ SMTP_PORT: config.SMTP_PORT,
+ SMTP_USER: config.SMTP_USER,
+ ALERT_FROM_EMAIL: config.ALERT_FROM_EMAIL,
+ ALERT_TO_EMAIL: config.ALERT_TO_EMAIL,
+ GLOBAL_EMAIL_ENABLED: config.GLOBAL_EMAIL_ENABLED
+ });
+
// Set toggle states based on current config
- const emailToggle = document.getElementById('email-enabled');
+ const emailToggle = document.getElementById('email-enabled') || document.getElementById('global-email-toggle');
if (emailToggle) {
const emailEnabled = config.GLOBAL_EMAIL_ENABLED === 'true' || config.GLOBAL_EMAIL_ENABLED === true;
emailToggle.checked = emailEnabled;
+ console.log('[Email Config] Email toggle set to:', emailEnabled);
+ } else {
+ console.log('[Email Config] Email toggle not found (looking for email-enabled or global-email-toggle)');
}
- const webhookToggle = document.getElementById('webhook-enabled');
+ const webhookToggle = document.getElementById('webhook-enabled') || document.getElementById('global-webhook-toggle');
if (webhookToggle) {
const webhookEnabled = config.GLOBAL_WEBHOOK_ENABLED === 'true' || config.GLOBAL_WEBHOOK_ENABLED === true;
webhookToggle.checked = webhookEnabled;
}
// Populate email fields with values from configuration
+ // Check both nested (smtp) and direct config locations
const emailFromInput = document.querySelector('input[name="ALERT_FROM_EMAIL"]');
- if (emailFromInput && smtp.from) {
- emailFromInput.value = smtp.from;
+ const fromValue = config.ALERT_FROM_EMAIL || smtp.from;
+ console.log('[Email Config] From input found:', !!emailFromInput, 'Value to set:', fromValue);
+ if (emailFromInput && fromValue) {
+ emailFromInput.value = fromValue;
}
const emailToInput = document.querySelector('input[name="ALERT_TO_EMAIL"]');
- if (emailToInput && smtp.to) {
- emailToInput.value = smtp.to;
+ const toValue = config.ALERT_TO_EMAIL || smtp.to;
+ console.log('[Email Config] To input found:', !!emailToInput, 'Value to set:', toValue);
+ if (emailToInput && toValue) {
+ emailToInput.value = toValue;
}
- const smtpHostInput = document.querySelector('input[name="ALERT_SMTP_HOST"]');
- if (smtpHostInput && smtp.host) {
- smtpHostInput.value = smtp.host;
+ // Populate SMTP fields
+ const smtpHostInput = document.querySelector('input[name="SMTP_HOST"]');
+ const hostValue = config.SMTP_HOST || smtp.host;
+ console.log('[Email Config] Host input found:', !!smtpHostInput, 'Value to set:', hostValue);
+ if (smtpHostInput && hostValue) {
+ smtpHostInput.value = hostValue;
}
- const smtpPortInput = document.querySelector('input[name="ALERT_SMTP_PORT"]');
- if (smtpPortInput && smtp.port) {
- smtpPortInput.value = smtp.port;
+ const smtpPortInput = document.querySelector('input[name="SMTP_PORT"]');
+ const portValue = config.SMTP_PORT || smtp.port;
+ console.log('[Email Config] Port input found:', !!smtpPortInput, 'Value to set:', portValue);
+ if (smtpPortInput && portValue) {
+ smtpPortInput.value = portValue;
}
- const smtpUserInput = document.querySelector('input[name="ALERT_SMTP_USER"]');
- if (smtpUserInput && smtp.user) {
- smtpUserInput.value = smtp.user;
+ const smtpUserInput = document.querySelector('input[name="SMTP_USER"]');
+ const userValue = config.SMTP_USER || smtp.user;
+ console.log('[Email Config] User input found:', !!smtpUserInput, 'Value to set:', userValue);
+ if (smtpUserInput && userValue) {
+ smtpUserInput.value = userValue;
}
- const smtpSecureInput = document.querySelector('input[name="ALERT_SMTP_SECURE"]');
- if (smtpSecureInput && smtp.secure !== undefined) {
- smtpSecureInput.checked = smtp.secure;
+ const smtpSecureCheckbox = document.querySelector('input[name="SMTP_SECURE"]');
+ const secureValue = config.SMTP_SECURE || smtp.secure;
+ if (smtpSecureCheckbox) {
+ smtpSecureCheckbox.checked = secureValue === true || secureValue === 'true';
}
- const smtpPassInput = document.querySelector('input[name="ALERT_EMAIL_PASSWORD"]');
-
- // Check if SMTP config exists (indicating password is saved)
- const hasSmtpConfig = smtp.host && smtp.user && smtp.from;
-
+ // Note: SMTP_PASS is not returned by the API for security reasons
+ // Show a placeholder to indicate it's already set
+ const smtpPassInput = document.querySelector('input[name="SMTP_PASS"]');
+ const hasSmtpConfig = hostValue || smtp.host;
if (smtpPassInput && hasSmtpConfig) {
- // Don't set a value, but update placeholder to indicate password is saved
- smtpPassInput.placeholder = 'Password saved (click to change)';
+ smtpPassInput.placeholder = '••••••••• (configured)';
}
- // Auto-detect email provider if from email is set
- if (emailFromInput && emailFromInput.value) {
- autoDetectEmailProvider(emailFromInput.value);
- }
-
- // Use existing email configuration from settings if available
- if (PulseApp.ui.settings && PulseApp.ui.settings.renderAlertsTab) {
- const fullContent = PulseApp.ui.settings.renderAlertsTab(config.alerts || {}, config);
-
- // Extract just the email configuration section
- const tempDiv = document.createElement('div');
- tempDiv.innerHTML = fullContent;
- const emailSection = tempDiv.querySelector('.bg-gray-50:nth-child(2)'); // Email section
-
- if (emailSection) {
- // Remove the outer container and just show the inner content
- const emailContent = emailSection.innerHTML;
- emailConfigSection.innerHTML = emailContent;
-
- // Initialize email functionality
- if (PulseApp.ui.settings.setupEmailProviderSelection) {
- setTimeout(() => PulseApp.ui.settings.setupEmailProviderSelection(), 100);
- }
- if (PulseApp.ui.settings.setupEmailTestButton) {
- setTimeout(() => PulseApp.ui.settings.setupEmailTestButton(), 100);
- }
- }
+ // Populate webhook configuration
+ const webhookUrlInput = document.querySelector('input[name="WEBHOOK_URL"]');
+ if (webhookUrlInput && config.WEBHOOK_URL) {
+ webhookUrlInput.value = config.WEBHOOK_URL;
}
}
@@ -1537,7 +2636,7 @@ PulseApp.ui.alertManagementModal = (() => {
function updateSSLHelp(provider, config) {
const sslLabel = document.querySelector('label[for="smtp-secure"]');
- const sslCheckbox = document.querySelector('input[name="ALERT_SMTP_SECURE"]');
+ const sslCheckbox = document.querySelector('input[name="SMTP_SECURE"]');
const advancedSettings = document.getElementById('advanced-smtp-settings');
if (sslLabel && sslCheckbox) {
@@ -1553,7 +2652,7 @@ PulseApp.ui.alertManagementModal = (() => {
}
function updatePasswordPlaceholder(provider) {
- const smtpPassInput = document.querySelector('input[name="ALERT_EMAIL_PASSWORD"]');
+ const smtpPassInput = document.querySelector('input[name="SMTP_PASS"]');
if (!smtpPassInput) return;
// Get current SMTP configuration
@@ -1651,9 +2750,9 @@ PulseApp.ui.alertManagementModal = (() => {
const passwordHelp = document.getElementById('smtp-password-help');
const usernameLabel = document.getElementById('smtp-username-label');
const usernameHelp = document.getElementById('smtp-username-help');
- const hostInput = document.querySelector('input[name="ALERT_SMTP_HOST"]');
- const portInput = document.querySelector('input[name="ALERT_SMTP_PORT"]');
- const secureCheckbox = document.querySelector('input[name="ALERT_SMTP_SECURE"]');
+ const hostInput = document.querySelector('input[name="SMTP_HOST"]');
+ const portInput = document.querySelector('input[name="SMTP_PORT"]');
+ const secureCheckbox = document.querySelector('input[name="SMTP_SECURE"]');
const providers = {
gmail: {
@@ -1736,7 +2835,7 @@ PulseApp.ui.alertManagementModal = (() => {
updatePasswordPlaceholder(provider);
// Update username placeholder and auto-fill
- const usernameInput = document.querySelector('input[name="ALERT_SMTP_USER"]');
+ const usernameInput = document.querySelector('input[name="SMTP_USER"]');
const fromEmailInput = document.getElementById('email-from-input');
if (usernameInput) {
@@ -1829,11 +2928,11 @@ PulseApp.ui.alertManagementModal = (() => {
const emailConfig = {
from: document.querySelector('input[name="ALERT_FROM_EMAIL"]')?.value,
to: document.querySelector('input[name="ALERT_TO_EMAIL"]')?.value,
- pass: document.querySelector('input[name="ALERT_EMAIL_PASSWORD"]')?.value, // Backend expects 'pass' not 'password'
- host: document.querySelector('input[name="ALERT_SMTP_HOST"]')?.value,
- port: document.querySelector('input[name="ALERT_SMTP_PORT"]')?.value,
- user: document.querySelector('input[name="ALERT_SMTP_USER"]')?.value,
- secure: document.querySelector('input[name="ALERT_SMTP_SECURE"]')?.checked
+ pass: document.querySelector('input[name="SMTP_PASS"]')?.value, // Backend expects 'pass' not 'password'
+ host: document.querySelector('input[name="SMTP_HOST"]')?.value,
+ port: document.querySelector('input[name="SMTP_PORT"]')?.value,
+ user: document.querySelector('input[name="SMTP_USER"]')?.value,
+ secure: document.querySelector('input[name="SMTP_SECURE"]')?.checked
};
if (!emailConfig.from || !emailConfig.to) {
@@ -1866,10 +2965,130 @@ PulseApp.ui.alertManagementModal = (() => {
}
async function saveEmailConfiguration() {
- // This function is called by the specific save button in the email section
- // It will trigger the main saveConfiguration function
- await saveConfiguration();
- PulseApp.ui.toast.success('Email configuration saved successfully!');
+ const saveBtn = document.getElementById('save-email-config-btn');
+ if (!saveBtn) return;
+
+ const originalText = saveBtn.textContent;
+ saveBtn.disabled = true;
+ saveBtn.textContent = 'Saving...';
+
+ try {
+ // Validate SMTP configuration
+ const validationErrors = validateSMTPConfiguration();
+ if (validationErrors.length > 0) {
+ const errorMessage = 'Please fix the following errors:\n\n• ' + validationErrors.join('\n• ');
+ alert(errorMessage);
+ saveBtn.textContent = originalText;
+ saveBtn.disabled = false;
+ return;
+ }
+
+ // Collect email configuration
+ const configToSave = {};
+ const smtpHost = document.querySelector('input[name="SMTP_HOST"]')?.value;
+ const smtpPort = document.querySelector('input[name="SMTP_PORT"]')?.value;
+ const smtpUser = document.querySelector('input[name="SMTP_USER"]')?.value;
+ const smtpPass = document.querySelector('input[name="SMTP_PASS"]')?.value;
+ const fromEmail = document.querySelector('input[name="ALERT_FROM_EMAIL"]')?.value;
+ const toEmail = document.querySelector('input[name="ALERT_TO_EMAIL"]')?.value;
+ const smtpSecure = document.querySelector('input[name="SMTP_SECURE"]')?.checked;
+ const emailEnabled = (document.getElementById('email-enabled') || document.getElementById('global-email-toggle'))?.checked;
+
+ // Only include values that are set
+ if (smtpHost) configToSave.SMTP_HOST = smtpHost;
+ if (smtpPort) configToSave.SMTP_PORT = smtpPort;
+ if (smtpUser) configToSave.SMTP_USER = smtpUser;
+ if (smtpPass) configToSave.SMTP_PASS = smtpPass;
+ if (fromEmail) configToSave.ALERT_FROM_EMAIL = fromEmail;
+ if (toEmail) configToSave.ALERT_TO_EMAIL = toEmail;
+ configToSave.SMTP_SECURE = smtpSecure ? 'true' : 'false';
+ configToSave.GLOBAL_EMAIL_ENABLED = emailEnabled ? 'true' : 'false';
+
+ // Save via API
+ const response = await fetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(configToSave)
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ PulseApp.ui.toast.success('Email configuration saved successfully!');
+ await loadConfiguration(); // Reload to reflect changes
+
+ // Update button to show success
+ saveBtn.textContent = 'Saved!';
+ saveBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
+ saveBtn.classList.add('bg-green-600');
+
+ setTimeout(() => {
+ saveBtn.textContent = originalText;
+ saveBtn.classList.remove('bg-green-600');
+ saveBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
+ saveBtn.disabled = false;
+ }, 2000);
+ } else {
+ throw new Error(result.error || 'Failed to save configuration');
+ }
+ } catch (error) {
+ console.error('Error saving email configuration:', error);
+ PulseApp.ui.toast.error('Failed to save email configuration');
+ saveBtn.textContent = originalText;
+ saveBtn.disabled = false;
+ }
+ }
+
+ async function saveWebhookConfiguration() {
+ const saveBtn = document.getElementById('save-webhook-btn');
+ if (!saveBtn) return;
+
+ const originalText = saveBtn.textContent;
+ saveBtn.disabled = true;
+ saveBtn.textContent = 'Saving...';
+
+ try {
+ // Collect webhook configuration
+ const configToSave = {};
+ const webhookUrl = document.querySelector('input[name="WEBHOOK_URL"]')?.value;
+ const webhookEnabled = (document.getElementById('webhook-enabled') || document.getElementById('global-webhook-toggle'))?.checked;
+
+ configToSave.WEBHOOK_URL = webhookUrl || '';
+ configToSave.GLOBAL_WEBHOOK_ENABLED = webhookEnabled ? 'true' : 'false';
+
+ // Save via API
+ const response = await fetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(configToSave)
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ PulseApp.ui.toast.success('Webhook configuration saved successfully!');
+ await loadConfiguration(); // Reload to reflect changes
+
+ // Update button to show success
+ saveBtn.textContent = 'Saved!';
+ saveBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
+ saveBtn.classList.add('bg-green-600');
+
+ setTimeout(() => {
+ saveBtn.textContent = originalText;
+ saveBtn.classList.remove('bg-green-600');
+ saveBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
+ saveBtn.disabled = false;
+ }, 2000);
+ } else {
+ throw new Error(result.error || 'Failed to save configuration');
+ }
+ } catch (error) {
+ console.error('Error saving webhook configuration:', error);
+ PulseApp.ui.toast.error('Failed to save webhook configuration');
+ saveBtn.textContent = originalText;
+ saveBtn.disabled = false;
+ }
}
function editSystemAlert(alertType) {
@@ -2746,20 +3965,31 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
}
function loadCurrentAlerts() {
- const contentDiv = document.getElementById('current-alerts-list');
+ // Try both old and new alert list containers for compatibility
+ let contentDiv = document.getElementById('active-alerts-list');
+ if (!contentDiv) {
+ contentDiv = document.getElementById('current-alerts-list');
+ }
if (!contentDiv) return;
// Get alerts from the alerts handler
if (PulseApp.alerts && PulseApp.alerts.getActiveAlerts) {
const activeAlerts = PulseApp.alerts.getActiveAlerts();
- renderCurrentAlerts(activeAlerts);
+ renderCurrentAlerts(activeAlerts, contentDiv);
+
+ // Update alert summary if we're in monitor tab
+ if (activeTab === 'monitor') {
+ updateAlertSummary();
+ }
} else {
- contentDiv.innerHTML = '
Alert system not initialized
';
+ contentDiv.innerHTML = '
Alert system not initialized
';
}
}
- function renderCurrentAlerts(alerts) {
- const contentDiv = document.getElementById('current-alerts-list');
+ function renderCurrentAlerts(alerts, contentDiv = null) {
+ if (!contentDiv) {
+ contentDiv = document.getElementById('active-alerts-list') || document.getElementById('current-alerts-list');
+ }
if (!contentDiv) return;
if (!alerts || alerts.length === 0) {
@@ -2916,7 +4146,7 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
${!acknowledged ? `
-
Acknowledge
@@ -2967,18 +4197,36 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
`;
}
- function loadAlertHistory() {
+ async function loadAlertHistory() {
const contentDiv = document.getElementById('alert-history-list');
if (!contentDiv) return;
- // Get historical alerts from the alerts handler
- if (PulseApp.alerts && PulseApp.alerts.getAlertHistory) {
- const historicalAlerts = PulseApp.alerts.getAlertHistory();
+ try {
+ // Show loading state
+ contentDiv.innerHTML = '
Loading alert history...
';
+
+ // Fetch actual alert history from the backend
+ const response = await fetch('/api/alerts/history?limit=100');
+ if (!response.ok) {
+ throw new Error('Failed to fetch alert history');
+ }
+
+ const data = await response.json();
+ const historicalAlerts = data.history || [];
+
renderAlertHistory(historicalAlerts);
- } else {
- // Mock historical data for now
- const mockHistory = generateMockAlertHistory();
- renderAlertHistory(mockHistory);
+
+ } catch (error) {
+ console.error('Error loading alert history:', error);
+ contentDiv.innerHTML = `
+
+
+
Error Loading History
+
Failed to load alert history: ${error.message}
+
+ `;
}
}
@@ -3185,6 +4433,75 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
}
}
+ function validateSMTPConfiguration() {
+ const errors = [];
+
+ // Get SMTP configuration values
+ const smtpHost = document.querySelector('input[name="SMTP_HOST"]')?.value?.trim();
+ const smtpPort = document.querySelector('input[name="SMTP_PORT"]')?.value?.trim();
+ const smtpUser = document.querySelector('input[name="SMTP_USER"]')?.value?.trim();
+ const smtpPass = document.querySelector('input[name="SMTP_PASS"]')?.value?.trim();
+ const fromEmail = document.querySelector('input[name="ALERT_FROM_EMAIL"]')?.value?.trim();
+ const toEmail = document.querySelector('input[name="ALERT_TO_EMAIL"]')?.value?.trim();
+
+ // Check if there's already a saved SMTP configuration
+ const existingSmtpConfig = currentConfig?.advanced?.smtp;
+ const hasExistingConfig = existingSmtpConfig?.host && existingSmtpConfig?.user;
+
+ // If all fields are empty and we have an existing config, skip validation
+ // This handles the case where the form hasn't been populated yet
+ const allFieldsEmpty = !smtpHost && !smtpPort && !smtpUser && !smtpPass && !fromEmail && !toEmail;
+ if (allFieldsEmpty && hasExistingConfig) {
+ return errors; // No errors, using existing config
+ }
+
+ // Validate required fields if any SMTP field is filled (partial configuration check)
+ const hasAnySMTPField = smtpHost || smtpPort || smtpUser || smtpPass || fromEmail || toEmail;
+
+ if (hasAnySMTPField) {
+ if (!smtpHost) {
+ errors.push('SMTP Host is required when configuring email');
+ }
+
+ if (!smtpPort) {
+ errors.push('SMTP Port is required when configuring email');
+ } else {
+ const port = parseInt(smtpPort);
+ if (isNaN(port) || port < 1 || port > 65535) {
+ errors.push('SMTP Port must be a valid port number (1-65535)');
+ }
+ }
+
+ if (!smtpUser) {
+ errors.push('SMTP Username is required when configuring email');
+ }
+
+ // Only require password for new configurations or if user is trying to change it
+ if (!smtpPass && !hasExistingConfig) {
+ errors.push('SMTP Password is required when configuring email');
+ }
+
+ if (!fromEmail) {
+ errors.push('From Email address is required when configuring email');
+ } else if (!isValidEmail(fromEmail)) {
+ errors.push('From Email must be a valid email address');
+ }
+
+ if (!toEmail) {
+ errors.push('To Email address is required when configuring email');
+ } else if (!isValidEmail(toEmail)) {
+ errors.push('To Email must be a valid email address');
+ }
+ }
+
+ return errors;
+ }
+
+ function isValidEmail(email) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ }
+
async function saveConfiguration() {
// The save configuration button in the alert management modal should
// save any pending changes. Since individual system alerts are edited
@@ -3204,17 +4521,46 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
try {
let configToSave = {};
let hasChanges = false;
-
- // Check for email configuration changes
+
+ // First check if we have any changes to save
const emailConfigSection = document.getElementById('primary-email-config');
+ let hasEmailChanges = false;
+
if (emailConfigSection) {
- const smtpHost = document.querySelector('input[name="ALERT_SMTP_HOST"]')?.value;
- const smtpPort = document.querySelector('input[name="ALERT_SMTP_PORT"]')?.value;
- const smtpUser = document.querySelector('input[name="ALERT_SMTP_USER"]')?.value;
- const smtpPass = document.querySelector('input[name="ALERT_EMAIL_PASSWORD"]')?.value;
+ const smtpHost = document.querySelector('input[name="SMTP_HOST"]')?.value;
+ const smtpPort = document.querySelector('input[name="SMTP_PORT"]')?.value;
+ const smtpUser = document.querySelector('input[name="SMTP_USER"]')?.value;
+ const smtpPass = document.querySelector('input[name="SMTP_PASS"]')?.value;
const fromEmail = document.querySelector('input[name="ALERT_FROM_EMAIL"]')?.value;
const toEmail = document.querySelector('input[name="ALERT_TO_EMAIL"]')?.value;
- const smtpSecure = document.querySelector('input[name="ALERT_SMTP_SECURE"]')?.checked;
+
+ // Check if any email field has a value (indicating user wants to save email config)
+ hasEmailChanges = smtpHost || smtpPort || smtpUser || smtpPass || fromEmail || toEmail;
+ }
+
+ // Only validate SMTP configuration if there are email changes
+ if (hasEmailChanges) {
+ const validationErrors = validateSMTPConfiguration();
+ if (validationErrors.length > 0) {
+ const errorMessage = 'Please fix the following errors:\n\n• ' + validationErrors.join('\n• ');
+ alert(errorMessage);
+
+ // Re-enable save button since validation failed
+ saveButton.disabled = false;
+ saveButton.textContent = originalText;
+ return;
+ }
+ }
+
+ // Process email configuration changes
+ if (emailConfigSection && hasEmailChanges) {
+ const smtpHost = document.querySelector('input[name="SMTP_HOST"]')?.value;
+ const smtpPort = document.querySelector('input[name="SMTP_PORT"]')?.value;
+ const smtpUser = document.querySelector('input[name="SMTP_USER"]')?.value;
+ const smtpPass = document.querySelector('input[name="SMTP_PASS"]')?.value;
+ const fromEmail = document.querySelector('input[name="ALERT_FROM_EMAIL"]')?.value;
+ const toEmail = document.querySelector('input[name="ALERT_TO_EMAIL"]')?.value;
+ const smtpSecure = document.querySelector('input[name="SMTP_SECURE"]')?.checked;
if (smtpHost) { configToSave.SMTP_HOST = smtpHost; hasChanges = true; }
if (smtpPort) { configToSave.SMTP_PORT = smtpPort; hasChanges = true; }
@@ -3287,7 +4633,12 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
'memory_usage_warning': 'memory',
'disk_space_warning': 'disk',
'disk_space_critical': 'disk', // Both disk alerts use same config
- 'guest_down': 'down'
+ 'guest_down': 'down',
+ // Also handle short form IDs that come from UI
+ 'cpu': 'cpu',
+ 'memory': 'memory',
+ 'disk': 'disk',
+ 'down': 'down'
};
const alertType = alertTypeMap[alertId];
@@ -3330,28 +4681,7 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
}
}
- async function editCustomAlert(alertId) {
- try {
- // First, fetch the current alert data
- const response = await fetch('/api/alerts/rules');
- if (!response.ok) {
- throw new Error(`Failed to fetch alert rules: ${response.status}`);
- }
-
- const data = await response.json();
- const alert = data.rules ? data.rules.find(rule => rule.id === alertId) : null;
-
- if (!alert) {
- throw new Error('Alert not found');
- }
-
- // Open the custom alert modal with pre-filled data
- openCustomAlertModal(alert.thresholds || [], alert);
-
- } catch (error) {
- console.error('Failed to load alert for editing:', error);
- }
- }
+ // editCustomAlert function removed - now using unified editAlertRule
async function deleteCustomAlert(alertId) {
try {
@@ -3574,17 +4904,17 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
closeModal,
switchTab: switchTab,
openCustomAlertModal,
- editCustomAlert,
+ editAlertRule,
toggleCustomAlert,
toggleAlert,
- toggleSystemAlert,
+ toggleAlertRule,
deleteCustomAlert,
showAlertDetails,
addWebhookEndpoint,
removeWebhookEndpoint,
handleEmailProviderSelection,
loadCurrentAlerts: () => {
- if (activeTab === 'alerts') {
+ if (activeTab === 'monitor' || activeTab === 'alerts') {
loadCurrentAlerts();
}
}
@@ -3598,5 +4928,4 @@ if (document.readyState === 'loading') {
PulseApp.ui.alertManagementModal.init();
}
-// Make toggleSystemAlert available globally for HTML onclick handlers
-window.toggleSystemAlert = PulseApp.ui.alertManagementModal.toggleSystemAlert;
\ No newline at end of file
+// Global functions are now exposed in the init() function to prevent timing issues
\ No newline at end of file