diff --git a/hbd/server/http.py b/hbd/server/http.py index 4ce5070..c40e3fe 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -537,6 +537,7 @@ async def start( hosts_with_plugins.append({ "name": hostname, "plugins": list(host.plugin_data.keys()), + "is_owner": _can_own_host(current_user, host), }) tmpl = env.get_template("plugins.html") diff --git a/hbd/server/main.py b/hbd/server/main.py index 2c62688..95701bd 100644 --- a/hbd/server/main.py +++ b/hbd/server/main.py @@ -101,9 +101,10 @@ async def reload_configuration(config_obj, config_path, components): access = config_mod.get_host_access(new_config, hostname) host.apply_access(access["owner"], access["managers"], access["monitors"]) - # Reload threshold checker + # Reload threshold checker and prune alerts orphaned by the new config if 'threshold_checker' in components: components['threshold_checker'].reload(new_config) + components['threshold_checker'].purge_stale_alerts(hbdclass) # Note: Changes to the following require restart: # - hb_port, hbd_port, ws_port (already bound) @@ -241,6 +242,10 @@ async def _run_async(config, config_path=None): ) udp.restore_connection_timers(hbdclass, restore_ctx) + # Drop alert states that no longer have a matching threshold (stale after + # upgrade or config change between runs). + threshold_checker.purge_stale_alerts(hbdclass) + # HTTP server (asyncio-based via aiohttp) try: http_task = asyncio.create_task( diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html index 41f1ff2..a74de89 100644 --- a/hbd/server/templates/plugins.html +++ b/hbd/server/templates/plugins.html @@ -131,6 +131,27 @@ text-overflow: ellipsis; } + .host-action-btn { + font-size: 0.75em; + font-weight: bold; + padding: 3px 10px; + border-radius: 4px; + border: none; + cursor: pointer; + text-decoration: none; + white-space: nowrap; + } + .host-action-btn.update-btn { + background: #e3f2fd; + color: #1565c0; + } + .host-action-btn.update-btn:hover { background: #bbdefb; } + .host-action-btn.delete-btn { + background: #ffebee; + color: #c62828; + } + .host-action-btn.delete-btn:hover { background: #ffcdd2; } + /* ── Host body ──────────────────────────────────────────────── */ .host-body { @@ -379,6 +400,14 @@ {% endif %} + {% if host.is_owner %} + Update + Delete + {% endif %} diff --git a/hbd/server/threshold.py b/hbd/server/threshold.py index fa4f5a1..9d86a00 100644 --- a/hbd/server/threshold.py +++ b/hbd/server/threshold.py @@ -1276,6 +1276,26 @@ class ThresholdChecker: alert_state.last_notification = now alert_state.notification_count += 1 + def purge_stale_alerts(self, hbdclass) -> None: + """Remove alert states that have no matching threshold configuration. + + Called after startup (pickle restore) and after each config reload so + that alerts orphaned by configuration changes do not linger forever. + Alerts whose metric_path is not present in the current threshold config + for that host are silently dropped. + """ + for hostname, host in hbdclass.Host.hosts.items(): + if not host.alert_states: + continue + configured = self.get_thresholds_for_host(hostname) + stale = [mp for mp in host.alert_states if mp not in configured] + for mp in stale: + logger.info( + "Purging stale alert state for %s / %s (no threshold configured)", + hostname, mp, + ) + del host.alert_states[mp] + def get_active_alerts(self, alert_states: Dict[str, AlertState]) -> list: """ Get all currently active (non-OK) alerts.