From d34ebeac5379688ddea8eba7dc3164b40d6257fb Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 10 Jun 2025 18:17:08 +0100 Subject: [PATCH] feat: comprehensive alert system simplification and UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplified alert rules: removed warning/critical levels, now one rule per metric type - Unified alert editing: all rules (built-in and custom) use same modal template - Enhanced UI: show actual thresholds/conditions in rules list without clicking edit - Fixed Create Rule button: changed from event listener to direct onclick approach - Fixed disabled rules visibility: disabled rules now stay in list with toggle off - Removed redundant labels: eliminated "Enabled/Disabled" badges (toggle shows state) - Updated alert summary cards: replaced Critical/Warning/Info with Active/Acknowledged/Rules/Status - Improved UX: consistent editing experience across all alert types - Cleaned up duplicate code: removed separate modal templates for system vs custom alerts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- data/acknowledgements.json | 511 +++---- server/alertManager.js | 210 ++- server/index.js | 41 + src/public/js/ui/alertManagementModal.js | 1715 +++++++++++++++++++--- 4 files changed, 1912 insertions(+), 565 deletions(-) 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 = (() => { - @@ -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

+
+ + + +
+
+ +
+
+
+

Acknowledged

+

0

+
+ + + +
+
+ +
+
+
+

Alert Rules

+

0

+
+ + + +
+
+ +
+
+
+

Monitoring

+

Active

+
+ + + +
+
+
+ + +
+
+
+ + + +

Active Alerts

+ 0 +
+
+ + +
+
+
+

Loading active alerts...

+
+
+ + +
+
+
+ + + +

Recent Activity

+ (Last 24 hours) +
+
+
+

Loading recent activity...

+
+
+
+ `; + } + + function renderConfigureTab() { + return ` +
+ +
+
+
+
+ + + +

Alert Rules

+
+
+ +
+
+
+ + +
+
+

Loading alert rules...

+
+
+
+ + +
+
+
+ + + +

Notification Settings

+
+
+ + +
+
+
+

Global Notification Controls

+

Master switches for all alert notifications

+
+
+ + +
+
+
+ + +
+

Email Configuration

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+
+ + +
+

Webhook 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

+ +
+ `; + 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

+ +
+ `; + 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 = (() => {
- +
- +
- +

(Usually your email address)

- +

(Your email password)

@@ -623,7 +1708,7 @@ PulseApp.ui.alertManagementModal = (() => {