From 1dbe0f8e647191da23932b19a5b344b74829b39d Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Mon, 11 May 2026 07:57:28 -0400 Subject: [PATCH] feat: replace YAML hosts editor with form-based CRUD table Settings > Hosts now renders a table with per-column controls (watch, dyndns, owner, managers/monitors multi-select, threshold config, notification channels) instead of a raw YAML textarea. Changes stage via the existing Publish flow like other form sections. Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/configio.py | 2 + hbd/server/http.py | 11 ++- hbd/server/settings.py | 16 +++- hbd/server/templates/settings.html | 123 ++++++++++++++++++++++++++++- tests/test_settings_sections.py | 12 ++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/hbd/server/configio.py b/hbd/server/configio.py index 5044688..4dc85a9 100644 --- a/hbd/server/configio.py +++ b/hbd/server/configio.py @@ -89,6 +89,8 @@ def apply_structured_section(data, section: str, values: dict) -> None: data[key] = values[key] elif section == "users": data["users"] = values + elif section == "hosts": + data["hosts"] = values else: raise ValueError(f"Unknown structured section: {section!r}") diff --git a/hbd/server/http.py b/hbd/server/http.py index 075e6d3..e05c4c9 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -1049,6 +1049,8 @@ async def start( title="Settings - Heartbeat", sections=settings_data["sections"], all_channel_names=settings_data["all_channel_names"], + all_usernames=settings_data["all_usernames"], + all_threshold_configs=settings_data["all_threshold_configs"], current_user=current_user.to_dict() if current_user else None, active_page="settings", ) @@ -1226,10 +1228,17 @@ async def start( attrs.pop("client_secret", None) data["oauth"] = new_oauth - for section in ("notification_channels", "thresholds", "hosts", "dns"): + for section in ("notification_channels", "thresholds", "dns"): if section in payload: configio_mod.apply_yaml_section(data, section, payload[section]) + if "hosts" in payload: + h = payload["hosts"] + if isinstance(h, dict): + configio_mod.apply_structured_section(data, "hosts", h) + else: + configio_mod.apply_yaml_section(data, "hosts", h) + configio_mod.write_config(_config_path, data) except Exception as exc: logger.error("Config write failed: %s", exc) diff --git a/hbd/server/settings.py b/hbd/server/settings.py index dfc1f33..ad6d265 100644 --- a/hbd/server/settings.py +++ b/hbd/server/settings.py @@ -436,7 +436,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list: "id": "hosts", "title": "Hosts", "description": "Host definitions loaded from the config file.", - "section_mode": "yaml", + "section_mode": "hosts", "api_section": "hosts", "hosts": hosts_list, "fields": [], @@ -475,4 +475,16 @@ def get_settings_data(config: dict, threshold_checker=None) -> dict: """Return sections list + auxiliary data for the settings template.""" sections = get_settings_sections(config, threshold_checker=threshold_checker) all_channel_names = sorted((config.get("notification_channels") or {}).keys()) - return {"sections": sections, "all_channel_names": all_channel_names} + all_usernames = sorted((config.get("users") or {}).keys()) + # Threshold config names come from the parsed section data already in sections. + tc_section = next((s for s in sections if s["id"] == "thresholds"), None) + all_threshold_configs = ( + [tc["name"] for tc in tc_section["threshold_configs"]] + if tc_section else [] + ) + return { + "sections": sections, + "all_channel_names": all_channel_names, + "all_usernames": all_usernames, + "all_threshold_configs": all_threshold_configs, + } diff --git a/hbd/server/templates/settings.html b/hbd/server/templates/settings.html index 916d908..806f65d 100644 --- a/hbd/server/templates/settings.html +++ b/hbd/server/templates/settings.html @@ -554,6 +554,68 @@ + {# ---- Hosts CRUD table ---- #} + {% elif section.section_mode == 'hosts' %} +
+ + + + + + + + + + + + + + {% for h in section.hosts %} + + + + + + + + + + + + {% endfor %} + +
HostnameWatchDynDNSOwnerManagersMonitorsThreshold configChannels
{{ h.name | e }} + + + + + + + +
+
+ + {# ---- Notification channels (form-based, live CRUD) ---- #} {% elif section.section_mode == 'channels' %} {% for f in section.fields %} @@ -666,8 +728,10 @@ {# /container #}