From f4231dd5f37a87ebc52ce07c3f79ba9bb62e812a Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Thu, 21 May 2026 11:18:05 -0400 Subject: [PATCH 01/15] fix: preserve log message order when replaying history on connect Send history messages newest-first from the server, tagged with history=True so the client appends rather than prepends them, avoiding reverse-chronological display on initial load. Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/templates/live.html | 2 +- hbd/server/ws.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hbd/server/templates/live.html b/hbd/server/templates/live.html index 4db57f0..1b39b65 100644 --- a/hbd/server/templates/live.html +++ b/hbd/server/templates/live.html @@ -540,7 +540,7 @@ if (msg.service) html += '' + msg.service + ''; html += '' + msg.message + ''; html += ''; - msgs.insertAdjacentHTML("afterbegin", html); + msgs.insertAdjacentHTML(state.history ? "beforeend" : "afterbegin", html); applyLogFilters(); } cnt++; diff --git a/hbd/server/ws.py b/hbd/server/ws.py index d073479..f8f9d6c 100644 --- a/hbd/server/ws.py +++ b/hbd/server/ws.py @@ -85,13 +85,15 @@ async def handler(request): except Exception as e: logger.error("Error sending initial hosts: %s", e) - # Send recent messages, filtered to hosts this user may see + # Send recent messages newest-first so the client can append them in + # display order without reordering on arrival (tagged history=True so + # the client knows to append rather than prepend). if data.msgs: try: - for m in data.msgs: + for m in reversed(data.msgs): host_name = m.get("host") if isinstance(m, dict) else None if not host_name or _user_can_see_host(user, host_name): - await ws.send_str(json.dumps({"type": "message", "data": m})) + await ws.send_str(json.dumps({"type": "message", "data": m, "history": True})) except Exception as e: logger.error("Error sending initial messages: %s", e) From 8729fe7038c6be1e75ce3cfa0b22f2421090ba14 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Thu, 21 May 2026 13:01:47 -0400 Subject: [PATCH 02/15] feat: sort hosts, thresholds, and channels alphabetically on settings page Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hbd/server/settings.py b/hbd/server/settings.py index 4ebc51f..50ae402 100644 --- a/hbd/server/settings.py +++ b/hbd/server/settings.py @@ -197,7 +197,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list: # ---- Notification channels (complex, built separately) ---------------- _METADATA_KEYS = {"type", "owner", "private", "min_level"} notif_channels = [] - for ch_name, ch_cfg in (config.get("notification_channels") or {}).items(): + for ch_name, ch_cfg in sorted((config.get("notification_channels") or {}).items()): if not isinstance(ch_cfg, dict): continue ch_type = ch_cfg.get("type", "") @@ -276,7 +276,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list: # ---- Hosts summary ---------------------------------------------------- hosts_list = [] - for hname, hcfg in (config.get("hosts") or {}).items(): + for hname, hcfg in sorted((config.get("hosts") or {}).items()): if not isinstance(hcfg, dict): continue hosts_list.append({ From fa317a3b782c12b5f82a2cd3d70ede1731d462b5 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Thu, 21 May 2026 22:33:37 -0400 Subject: [PATCH 03/15] feat: add dark mode with light/dark/auto theme setting Theme preference stored in localStorage (auto follows the OS setting). The chosen data-theme attribute is applied synchronously in to avoid any flash of unstyled content. CSS custom properties handle all surface, text, border and input colours across every page. The Appearance section on the profile page lets each user switch modes. Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/templates/about.html | 13 ++++ hbd/server/templates/alerts.html | 25 +++++++ hbd/server/templates/head.html | 106 +++++++++++++++++++++++++---- hbd/server/templates/live.html | 25 +++++++ hbd/server/templates/plugins.html | 41 +++++++++++ hbd/server/templates/profile.html | 85 +++++++++++++++++++++++ hbd/server/templates/settings.html | 61 +++++++++++++++++ 7 files changed, 344 insertions(+), 12 deletions(-) diff --git a/hbd/server/templates/about.html b/hbd/server/templates/about.html index 94c8290..19c6c39 100644 --- a/hbd/server/templates/about.html +++ b/hbd/server/templates/about.html @@ -100,6 +100,19 @@ } .logo-text { flex: 1; } + + /* ── Dark mode ── */ + html[data-theme="dark"] h1 { color: var(--text); } + html[data-theme="dark"] .subtitle { color: var(--text-sec); } + html[data-theme="dark"] .section { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); } + html[data-theme="dark"] .section h2 { color: var(--text); border-bottom-color: var(--border); } + html[data-theme="dark"] .info-row { border-bottom-color: var(--border-4); } + html[data-theme="dark"] .info-label { color: var(--text-sec); } + html[data-theme="dark"] .info-value { color: var(--text); } + html[data-theme="dark"] .info-value a { color: var(--link); } + html[data-theme="dark"] .hb-logo { color: var(--link); } + html[data-theme="dark"] .hb-tagline { color: var(--text-sec); } + html[data-theme="dark"] .version-badge { background: #1a3255; color: #60a5fa; } diff --git a/hbd/server/templates/alerts.html b/hbd/server/templates/alerts.html index efb28e6..6e9811a 100644 --- a/hbd/server/templates/alerts.html +++ b/hbd/server/templates/alerts.html @@ -305,6 +305,31 @@ text-align: right; margin-bottom: 15px; } + + /* ── Dark mode ── */ + html[data-theme="dark"] h1 { color: var(--text); } + html[data-theme="dark"] .subtitle { color: var(--text-sec); } + html[data-theme="dark"] .summary-card { background: var(--surface); } + html[data-theme="dark"] .summary-label { color: var(--text-sec); } + html[data-theme="dark"] .filters { background: var(--surface); } + html[data-theme="dark"] .filter-label { color: var(--text-sec); } + html[data-theme="dark"] .filter-button { background: var(--surface-2); border-color: var(--border); color: var(--text); } + html[data-theme="dark"] .filter-button.active { background: #2196f3; color: #fff; border-color: #2196f3; } + html[data-theme="dark"] .filter-input { background: var(--input-bg); border-color: var(--input-border); color: var(--text); } + html[data-theme="dark"] .alerts-container { background: var(--surface); } + html[data-theme="dark"] .alert-item { background: var(--surface-2); } + html[data-theme="dark"] .alert-item.acknowledged { background: var(--surface-3); } + html[data-theme="dark"] .alert-item.critical { background: #2e0a0a; border-left-color: #f44336; } + html[data-theme="dark"] .alert-item.warning { background: #2e1a00; border-left-color: #ff9800; } + html[data-theme="dark"] .alert-item.unknown { background: var(--surface-2); } + html[data-theme="dark"] .alert-hostname { color: var(--link); } + html[data-theme="dark"] .alert-details { color: var(--text-sec); } + html[data-theme="dark"] .alert-value { color: var(--text); } + html[data-theme="dark"] .alert-duration { color: var(--text-muted); } + html[data-theme="dark"] .last-update { color: var(--text-sec); } + html[data-theme="dark"] .refresh-info { color: var(--text-muted); border-top-color: var(--border); } + html[data-theme="dark"] .no-alerts, + html[data-theme="dark"] .loading { color: var(--text-muted); } diff --git a/hbd/server/templates/head.html b/hbd/server/templates/head.html index 517fc17..8e7c27d 100644 --- a/hbd/server/templates/head.html +++ b/hbd/server/templates/head.html @@ -5,7 +5,68 @@ {{ title }} {% if extra_scripts %}{% endif %} +