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>
This commit is contained in:
2026-05-09 06:46:13 -04:00
parent 43487f17e7
commit 338711181b
3 changed files with 54 additions and 4 deletions
+2 -2
View File
@@ -641,7 +641,7 @@ async def start(
<meta charset="utf-8">
<title>Heartbeat — Login</title>
<style>
body {{ font-family: sans-serif; background: #16191d; display: flex;
body {{ font-family: sans-serif; background: #f5f5f5; display: flex;
justify-content: center; align-items: center; height: 100vh; margin: 0; }}
.box {{ background: #fff; padding: 2em 2.5em; border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,.15); min-width: 300px; }}
@@ -657,7 +657,7 @@ async def start(
.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: flex; align-items: center; justify-content: center;
gap: .5em; width: 100%; padding: .6em; background: #609926;
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; }}
+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>