Compare commits

...

4 Commits

Author SHA1 Message Date
andreas 58c2b9d996 version 5.2.6
Release / release (push) Successful in 5s
2026-05-09 06:56:00 -04:00
andreas 2e8bcb630d fix: show human-readable duration in re-notification messages
Replace raw seconds with d h m s format in "ongoing for ..." strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 06:53:41 -04:00
andreas 338711181b feat: alerts host-filter field with URL query param and notify URL
- Add regex filter input to the Alerts dashboard that filters displayed
  hosts on every keystroke; invalid regex turns the border red
- Initialise the filter from ?filter= in the URL query string
- Change _build_url() to produce /alerts?filter=<hostname> so
  notification links (Pushover, email, Matrix, etc.) land on the
  alerts page pre-filtered to the alerting host

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 06:46:13 -04:00
andreas 43487f17e7 feat: optional logo on Gitea OAuth login button
Reads oauth.gitea.logo from config and, when set, renders an <img>
inside the button with flex alignment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 06:24:27 -04:00
7 changed files with 74 additions and 9 deletions
+1 -1
View File
@@ -14,4 +14,4 @@ Install options:
"""
__all__ = ["__version__"]
__version__ = "5.2.5"
__version__ = "5.2.6"
+6 -2
View File
@@ -627,10 +627,12 @@ async def start(
gitea_button = ""
if oauth_mod.is_enabled(config):
logo_url = config.get("oauth", {}).get("gitea", {}).get("logo", "")
logo_img = f'<img src="{logo_url}" alt="" class="gitea-logo">' if logo_url else ""
gitea_button = f"""
<div class="divider">or</div>
<a href="/login/oauth/gitea" class="gitea-btn">
Sign in with Gitea
{logo_img}Sign in with Gitea
</a>"""
html = f"""<!DOCTYPE html>
@@ -654,10 +656,12 @@ async def start(
.field {{ margin-bottom: .9em; }}
.divider {{ text-align: center; margin: 1.2em 0 .8em; color: #999;
font-size: .85em; border-top: 1px solid #eee; padding-top: .8em; }}
.gitea-btn {{ display: block; width: 100%; padding: .6em; background: #609926;
.gitea-btn {{ display: flex; align-items: center; justify-content: center;
gap: .5em; width: 100%; padding: .6em; background: #16191d;
color: #fff; border-radius: 4px; font-size: 1em; text-align: center;
text-decoration: none; box-sizing: border-box; }}
.gitea-btn:hover {{ background: #4e7d1e; }}
.gitea-logo {{ height: 1.2em; width: auto; vertical-align: middle; }}
</style>
</head>
<body>
+1 -1
View File
@@ -401,7 +401,7 @@ def _build_url(host_name: str) -> str:
base_url = _config.get("base_url", "").rstrip("/")
if not base_url:
return ""
return f"{base_url}/plugins#{host_name}"
return f"{base_url}/alerts?filter={host_name}"
async def send_notification(host_name: str, notif: Notification) -> dict:
+51 -1
View File
@@ -94,6 +94,24 @@
border-color: #2196f3;
}
.filter-input {
padding: 7px 12px;
border: 2px solid #ddd;
border-radius: 20px;
font-size: 0.9em;
outline: none;
width: 200px;
transition: border-color 0.2s;
}
.filter-input:focus {
border-color: #2196f3;
}
.filter-input.invalid {
border-color: #f44336;
}
.alerts-container {
background: white;
border-radius: 8px;
@@ -316,6 +334,7 @@
<button class="filter-button active" onclick="filterAlerts('all')">All</button>
<button class="filter-button" onclick="filterAlerts('critical')">Critical Only</button>
<button class="filter-button" onclick="filterAlerts('warning')">Warning Only</button>
<input id="host-filter" class="filter-input" type="text" placeholder="host filter (regex)" oninput="onHostFilterInput(this)">
</div>
<div class="alerts-container">
@@ -332,6 +351,7 @@
<script>
let currentFilter = 'all';
let allAlerts = [];
let hostFilterRe = null;
async function loadAlerts() {
try {
@@ -366,10 +386,13 @@
// Filter alerts based on current filter
let filteredAlerts = alerts;
if (currentFilter !== 'all') {
filteredAlerts = alerts.filter(alert =>
filteredAlerts = filteredAlerts.filter(alert =>
alert.level.toLowerCase() === currentFilter
);
}
if (hostFilterRe) {
filteredAlerts = filteredAlerts.filter(alert => hostFilterRe.test(alert.hostname));
}
if (filteredAlerts.length === 0) {
if (currentFilter === 'all' && alerts.length === 0) {
@@ -538,9 +561,36 @@
}
}
function onHostFilterInput(input) {
const val = input.value.trim();
if (!val) {
hostFilterRe = null;
input.classList.remove('invalid');
} else {
try {
hostFilterRe = new RegExp(val, 'i');
input.classList.remove('invalid');
} catch (_) {
hostFilterRe = null;
input.classList.add('invalid');
}
}
renderAlerts(allAlerts);
}
// Auto-refresh every 15 seconds
setInterval(loadAlerts, 15000);
// Initialise filter from URL query string (?filter=...)
(function () {
const param = new URLSearchParams(window.location.search).get('filter');
if (param) {
const input = document.getElementById('host-filter');
input.value = param;
onHostFilterInput(input);
}
})();
// Initial load
loadAlerts();
</script>
+13 -2
View File
@@ -1393,6 +1393,17 @@ class ThresholdChecker:
else:
self._check_renotify(host_name, alert_state, metric_path, value, threshold, plugin_data, check_name=check_name, metric_name=metric_name)
@staticmethod
def _human_duration(seconds: float) -> str:
s = int(seconds)
if s < 120:
return f"{s}s"
if s < 3600:
return f"{s // 60}m {s % 60}s"
h, rem = divmod(s, 3600)
m = rem // 60
return f"{h}h {m}m" if m else f"{h}h"
def _check_renotify(
self,
host_name: str,
@@ -1454,9 +1465,9 @@ class ThresholdChecker:
check_name=check_name,
metric_name=metric_name,
)
body = f"{value} {threshold_info}, ongoing for {int(now - alert_state.since)}s"
body = f"{value} {threshold_info}, ongoing for {self._human_duration(now - alert_state.since)}"
else:
body = f"{value} (ongoing for {int(now - alert_state.since)}s)"
body = f"{value} (ongoing for {self._human_duration(now - alert_state.since)})"
message = f"REMINDER ({alert_state.level.name}): {host_name} - {short_path} = {body}"
from . import hbdclass
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hbd"
version = "5.2.5"
version = "5.2.6"
description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
readme = "README.md"
requires-python = ">=3.11"
+1 -1
View File
@@ -41,7 +41,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# updated by scripts/bumpminor.sh
__version__ = "5.2.5"
__version__ = "5.2.6"
# ---------------------------------------------------------------------------
# Protocol (mirrors hbd/common/proto.py)