{{ f.description }}
{% endif %} +diff --git a/hbd/server/configio.py b/hbd/server/configio.py index 4dc85a9..b5902cc 100644 --- a/hbd/server/configio.py +++ b/hbd/server/configio.py @@ -21,6 +21,7 @@ _SERVER_KEYS = [ "interval", "grace", "base_url", "threshold_renotify_interval", "logfile", "pidfile", "pickfile", "journal_enabled", "journal_dir", "journal_max_size", "journal_max_backups", "default_owner", + "default_threshold_config", ] # Top-level keys managed by the 'dns' logical section diff --git a/hbd/server/http.py b/hbd/server/http.py index e05c4c9..5036bbf 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -25,6 +25,74 @@ logger = logging.getLogger(__name__) eventlog = notify_mod.eventlog + +def _build_threshold_configs_from_form(form_data: dict) -> dict: + """Convert form-submitted flat threshold data to nested threshold_configs YAML structure. + + Input: {config_name: {metric_path: {warning, critical, operator, hysteresis, enabled, count, display}}} + Output: {config_name: {thresholds: {plugin: {metric: {warning, critical, ...}}}}} + """ + result = {} + for config_name, metrics in form_data.items(): + if not isinstance(metrics, dict): + continue + thresholds = {} + for metric_path, values in metrics.items(): + _insert_threshold_metric(thresholds, metric_path, values) + result[config_name] = {"thresholds": thresholds} + return result + + +def _insert_threshold_metric(thresholds: dict, metric_path: str, values: dict) -> None: + """Insert a single metric into the nested threshold YAML structure.""" + if not isinstance(values, dict): + return + + cfg = {} + op = values.get("operator", ">") + if op and op != ">": + cfg["operator"] = op + + for key, cast in (("warning", float), ("critical", float), ("hysteresis", float)): + v = values.get(key) + if v is not None: + try: + cfg[key] = cast(v) + except (TypeError, ValueError): + pass + + count = values.get("count") + if count is not None: + try: + cfg["count"] = int(count) + except (TypeError, ValueError): + pass + + display = values.get("display", "") + if display: + cfg["display"] = display + + if not values.get("enabled", True): + cfg["enabled"] = False + + parts = metric_path.split(".", 2) + + if len(parts) == 1: + # e.g. "rtt" + thresholds[metric_path] = cfg + elif len(parts) == 2: + plugin, metric = parts + thresholds.setdefault(plugin, {})[metric] = cfg + else: + plugin, intermediate, leaf = parts + thresholds.setdefault(plugin, {}) + if plugin == "disk_monitor": + thresholds[plugin].setdefault("partitions", {}).setdefault(intermediate, {})[leaf] = cfg + elif plugin == "zfs_monitor": + thresholds[plugin].setdefault("pools", {}).setdefault(intermediate, {})[leaf] = cfg + else: + thresholds[plugin].setdefault(intermediate, {})[leaf] = cfg + def _render_template(html_str: str, **context) -> str: tmpl = jinja2.Template(html_str) return tmpl.render(**context) @@ -1228,10 +1296,17 @@ async def start( attrs.pop("client_secret", None) data["oauth"] = new_oauth - for section in ("notification_channels", "thresholds", "dns"): + for section in ("notification_channels", "dns"): if section in payload: configio_mod.apply_yaml_section(data, section, payload[section]) + if "thresholds" in payload: + tc = payload["thresholds"] + if isinstance(tc, str): + configio_mod.apply_yaml_section(data, "thresholds", tc) + elif isinstance(tc, dict): + data["threshold_configs"] = _build_threshold_configs_from_form(tc) + if "hosts" in payload: h = payload["hosts"] if isinstance(h, dict): diff --git a/hbd/server/settings.py b/hbd/server/settings.py index 5200ef8..aef014d 100644 --- a/hbd/server/settings.py +++ b/hbd/server/settings.py @@ -247,6 +247,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list: "hysteresis": tc.hysteresis, "count": tc.count, "enabled": tc.enabled, + "display": tc.display or "", } threshold_config_list = [] @@ -448,12 +449,12 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list: "id": "thresholds", "title": "Threshold Configurations", "description": "Named alert threshold sets. Each defines warning/critical levels per metric.", - "section_mode": "yaml", + "section_mode": "thresholds", "api_section": "thresholds", "threshold_configs": threshold_config_list, "fields": [ field("default_threshold_config", "Default config", "text", - "Threshold config used for hosts with no explicit mapping."), + "Threshold config used for hosts with no explicit mapping.", editable=True), ], }, { diff --git a/hbd/server/templates/settings.html b/hbd/server/templates/settings.html index 1f35e89..49e0d28 100644 --- a/hbd/server/templates/settings.html +++ b/hbd/server/templates/settings.html @@ -383,6 +383,29 @@ .backup-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: .87em; } .backup-row:last-child { border-bottom: none; } + /* ---- Threshold config cards ---- */ + .thresh-cfg-card { + margin-bottom: 14px; + border: 1px solid #e0e0e0; + border-radius: 6px; + overflow: hidden; + } + .thresh-cfg-header { + background: #f5f5f5; + padding: 8px 14px; + display: flex; + align-items: center; + gap: 10px; + border-bottom: 1px solid #e0e0e0; + } + .thresh-cfg-name-label { + font-weight: 600; + font-size: 0.9em; + color: #1a237e; + } + .thresh-metric-table { width: 100%; } + .thresh-metric-table th { white-space: nowrap; } + /* ---- Multi-picker ---- */ .mpick-wrapper { display: block; } .mpick-display { @@ -689,6 +712,84 @@ + {# ---- Threshold configurations (form-based) ---- #} + {% elif section.section_mode == 'thresholds' %} + {% for f in section.fields %} +
{{ f.description }}
{% endif %} +| Metric path | Op | +Warning | Critical | +Hysteresis | Count | +Display | +En | + |
|---|---|---|---|---|---|---|---|---|
| {{ m.metric | e }} | ++ + | ++ | + | + | + | + | + | + |
| Metric path | Op | +Warning | Critical | +Hysteresis | Count | +Display | +En | + |
|---|