diff --git a/.hb.yaml b/.hb.yaml index 61ef9b6..eb3f778 100644 --- a/.hb.yaml +++ b/.hb.yaml @@ -99,7 +99,7 @@ hosts: dyndns: true weekend: - threshold_config: default + threshold_config: freebsd_server watch: false notification_channels: [pushover_standard] dyndns: true @@ -159,7 +159,7 @@ threshold_configs: warning: 85.0 critical: 90.0 rtt: - warning: 30 + warning: 50 critical: 250.0 @@ -170,9 +170,9 @@ threshold_configs: warning: 80.0 critical: 90.0 memory_monitor: - percent: - warning: 3.0 - critical: 95.0 + memory_percent: + warning: 97.0 + critical: 100.0 disk_monitor: partitions: /: @@ -188,12 +188,12 @@ threshold_configs: warning: WARNING critical: CRITICAL operator: "==" - UPS_load: - display: "{ups_output}" + ups_load: + display: "load to high: {ups_output}" warning: 70 critical: 80 operator: ">=" - UPS_status_code: + ups_status_code: display: "{ups_output}" warning: 1 critical: 2 @@ -204,7 +204,7 @@ threshold_configs: critical: 2 operator: ">=" rtt: - warning: 30 + warning: 50 critical: 250.0 truenas_server: @@ -232,13 +232,13 @@ threshold_configs: warning: WARNING critical: CRITICAL operator: "==" - UPS_load: - display: "{ups_output}" - warning: 70 - critical: 80 - operator: ">=" - UPS_status_code: - display: "{ups_output}" + ups_load: + display: "load to high: {ups_output}" + WARNING: 70 + CRITICAL: 80 + OPERATOR: ">=" + ups_status_code: + DISPLAY: "{ups_output}" warning: 1 critical: 2 operator: ">=" @@ -248,7 +248,7 @@ threshold_configs: critical: 2 operator: ">=" rtt: - warning: 100 + warning: 120 critical: 250.0 diff --git a/hbd/server/hbdclass.py b/hbd/server/hbdclass.py index b8e45df..da23962 100644 --- a/hbd/server/hbdclass.py +++ b/hbd/server/hbdclass.py @@ -307,6 +307,21 @@ class Host: d["name"] = "%s" % d["name"] d["dyn"] = str(self.dyn) d["num"] = self.num + + # Add alert counts + warning_count = 0 + critical_count = 0 + for metric_path, alert_state in self.alert_states.items(): + # Import AlertLevel here to avoid circular imports + from .threshold import AlertLevel + if alert_state.level == AlertLevel.WARNING: + warning_count += 1 + elif alert_state.level == AlertLevel.CRITICAL: + critical_count += 1 + + d["alert_warning_count"] = warning_count + d["alert_critical_count"] = critical_count + for c in ["IPv4", "IPv6"]: if c in self.connections: cs = self.connections[c].statedict() @@ -363,6 +378,21 @@ class Host: ddict[d] = cl else: ddict[d] = self.__dict__[d] + + # Add alert counts (computed from alert_states) + warning_count = 0 + critical_count = 0 + if hasattr(self, 'alert_states'): + from .threshold import AlertLevel + for metric_path, alert_state in self.alert_states.items(): + if alert_state.level == AlertLevel.WARNING: + warning_count += 1 + elif alert_state.level == AlertLevel.CRITICAL: + critical_count += 1 + + ddict["alert_warning_count"] = warning_count + ddict["alert_critical_count"] = critical_count + return ddict def jsons(self): diff --git a/hbd/server/http.py b/hbd/server/http.py index 20b8678..5728e9b 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -323,6 +323,49 @@ async def start( "summary": summary, "host_count": len(hbdclass.Host.hosts), }) + + async def api_acknowledge_alert(request): + """Acknowledge an alert to stop reminder notifications.""" + try: + data = await request.json() + except Exception: + return web.json_response( + {"error": "Invalid JSON in request body"}, + status=400 + ) + + hostname = data.get("hostname") + metric_path = data.get("metric_path") + + if not hostname or not metric_path: + return web.json_response( + {"error": "Missing required fields: hostname and metric_path"}, + status=400 + ) + + if hostname not in hbdclass.Host.hosts: + return web.json_response( + {"error": f"Host '{hostname}' not found"}, + status=404 + ) + + host = hbdclass.Host.hosts[hostname] + + if metric_path not in host.alert_states: + return web.json_response( + {"error": f"Alert '{metric_path}' not found for host '{hostname}'"}, + status=404 + ) + + alert_state = host.alert_states[metric_path] + alert_state.acknowledge() + + return web.json_response({ + "success": True, + "hostname": hostname, + "metric_path": metric_path, + "acknowledged_at": alert_state.acknowledged_at, + }) # ------------------------------------------------------------------------- # UI Pages @@ -375,6 +418,7 @@ async def start( web.get("/api/0/hosts/{hostname}/plugins/{plugin_name}", api_host_plugin_detail), web.get("/api/0/hosts/{hostname}/alerts", api_host_alerts), web.get("/api/0/alerts", api_all_alerts), + web.post("/api/0/alerts/acknowledge", api_acknowledge_alert), web.get("/c", cmd), web.get("/d", drop), web.get("/n", register), diff --git a/hbd/server/templates/alerts.html b/hbd/server/templates/alerts.html index 1209480..5297722 100644 --- a/hbd/server/templates/alerts.html +++ b/hbd/server/templates/alerts.html @@ -153,6 +153,11 @@ align-items: center; transition: all 0.2s; } + + .alert-item.acknowledged { + opacity: 0.6; + background: #f0f0f0; + } .alert-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); @@ -237,6 +242,46 @@ color: #999; font-size: 0.85em; } + + .alert-actions { + display: flex; + flex-direction: column; + gap: 8px; + margin-left: 15px; + } + + .acknowledge-btn { + padding: 8px 16px; + background: #2196f3; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.85em; + transition: all 0.2s; + white-space: nowrap; + } + + .acknowledge-btn:hover { + background: #1976d2; + transform: scale(1.05); + } + + .acknowledge-btn:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; + } + + .acknowledged-badge { + padding: 4px 8px; + background: #4caf50; + color: white; + border-radius: 4px; + font-size: 0.75em; + text-align: center; + white-space: nowrap; + } .no-alerts { text-align: center; @@ -396,6 +441,7 @@ function renderAlert(alert) { const level = alert.level.toLowerCase(); const duration = getDuration(alert.since); + const acknowledged = alert.acknowledged || false; // Use formatted message if available, otherwise build from individual fields let valueText = `Value: ${formatValue(alert.last_value)}`; @@ -405,8 +451,26 @@ valueText += ` (threshold: ${alert.operator} ${formatValue(alert.threshold_value)})`; } + // Build actions section + let actionsHtml = ''; + if (acknowledged) { + actionsHtml = ` +