diff --git a/hbd/server/http.py b/hbd/server/http.py
index 5b720ac..78f9947 100644
--- a/hbd/server/http.py
+++ b/hbd/server/http.py
@@ -143,6 +143,25 @@ def _build_host_info(host, threshold_checker=None) -> dict:
thresholds = None
if threshold_checker is not None:
raw = threshold_checker.get_thresholds_for_host(host.name)
+
+ # Build reverse coverage: which metric paths suffix-match to each threshold.
+ # Mirrors the logic in ThresholdChecker._find_threshold.
+ coverage: dict = {}
+ for plugin_name, samples in host.plugin_data.items():
+ if not samples:
+ continue
+ _, pdata = samples[-1]
+ for field_name in pdata:
+ full_path = f"{plugin_name}.{field_name}"
+ if full_path in raw:
+ continue # exact match — the threshold IS this metric
+ parts = field_name.split("_")
+ for i in range(1, len(parts)):
+ candidate = f"{plugin_name}." + "_".join(parts[i:])
+ if candidate in raw:
+ coverage.setdefault(candidate, []).append(full_path)
+ break
+
thresholds = sorted(
[
{
@@ -150,6 +169,7 @@ def _build_host_info(host, threshold_checker=None) -> dict:
"warning": tc.warning,
"critical": tc.critical,
"operator": tc.operator.value,
+ "covers": sorted(coverage.get(tc.metric_path, [])),
}
for tc in raw.values()
],
diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html
index 8a340b1..5171553 100644
--- a/hbd/server/templates/plugins.html
+++ b/hbd/server/templates/plugins.html
@@ -411,6 +411,7 @@
}
.info-note { color: #888; font-style: italic; }
.info-loading { color: #bbb; font-style: italic; }
+ .threshold-covers { font-size: 0.85em; color: #777; font-style: italic; }
@@ -588,8 +589,12 @@
for (const t of data.thresholds) {
const w = t.warning != null ? escHtml(String(t.warning)) : '—';
const c = t.critical != null ? escHtml(String(t.critical)) : '—';
+ let metricCell = escHtml(t.metric);
+ if (t.covers && t.covers.length > 0) {
+ metricCell += `
↳ ${t.covers.map(escHtml).join(', ')}`;
+ }
html += `
- | ${escHtml(t.metric)} |
+ ${metricCell} |
${escHtml(t.operator)} |
${w} |
${c} |
diff --git a/tests/test_http_host_info.py b/tests/test_http_host_info.py
index 39b2171..9a5df61 100644
--- a/tests/test_http_host_info.py
+++ b/tests/test_http_host_info.py
@@ -11,12 +11,13 @@ class _FakeConn:
class _FakeHost:
def __init__(self, name="myhost", owner=None, managers=None,
- connections=None, os_data=None):
+ connections=None, os_data=None, plugin_data=None):
self.name = name
self.owner = owner
self.managers = managers or []
self.connections = connections or {}
self._os_data = os_data
+ self.plugin_data = plugin_data or {}
def get_latest_plugin_data(self, plugin_name):
if plugin_name == "os_info" and self._os_data is not None:
@@ -127,3 +128,47 @@ def test_build_host_info_nagios_operator_serialized():
host = _FakeHost()
result = _build_host_info(host, threshold_checker=checker)
assert result["thresholds"][0]["operator"] == "nagios"
+
+
+def test_build_host_info_covers_suffix_matched_metrics():
+ """memory_monitor.percent threshold covers swap_percent via suffix match."""
+ from hbd.server.threshold import ThresholdConfig
+ tc_pct = ThresholdConfig("memory_monitor.percent", warning=85.0, critical=95.0)
+ checker = MagicMock()
+ checker.get_thresholds_for_host.return_value = {"memory_monitor.percent": tc_pct}
+
+ host = _FakeHost(
+ connections={},
+ os_data=None,
+ )
+ # Simulate plugin_data with both percent and swap_percent fields
+ host.plugin_data = {
+ "memory_monitor": [(1234567890.0, {
+ "percent": 80.0,
+ "swap_percent": 25.0,
+ "available_mb": 2000,
+ })]
+ }
+
+ result = _build_host_info(host, threshold_checker=checker)
+ assert result["thresholds"] is not None
+ t = result["thresholds"][0]
+ assert t["metric"] == "memory_monitor.percent"
+ assert t["covers"] == ["memory_monitor.swap_percent"]
+
+
+def test_build_host_info_covers_empty_when_exact_matches_only():
+ """No covers when all plugin fields match their threshold exactly."""
+ from hbd.server.threshold import ThresholdConfig
+ tc_pct = ThresholdConfig("memory_monitor.percent", warning=85.0, critical=95.0)
+ checker = MagicMock()
+ checker.get_thresholds_for_host.return_value = {"memory_monitor.percent": tc_pct}
+
+ host = _FakeHost()
+ host.plugin_data = {
+ "memory_monitor": [(1234567890.0, {"percent": 80.0})]
+ }
+
+ result = _build_host_info(host, threshold_checker=checker)
+ t = result["thresholds"][0]
+ assert t["covers"] == []