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.