Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca5ef384a8 | |||
| c93dbdc0f4 | |||
| 3a546a1e5c |
+1
-1
@@ -14,4 +14,4 @@ Install options:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = ["__version__"]
|
__all__ = ["__version__"]
|
||||||
__version__ = "5.1.17"
|
__version__ = "5.1.18"
|
||||||
|
|||||||
+3
-1
@@ -474,6 +474,7 @@ async def cleanup(connections: List[AsyncConnection]):
|
|||||||
logger.error(f"Error sending shutdown: {e}")
|
logger.error(f"Error sending shutdown: {e}")
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
break # Only send shutdown on first connection to avoid duplicates
|
||||||
|
|
||||||
# Give messages time to send
|
# Give messages time to send
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
@@ -540,6 +541,7 @@ async def async_main(args, config):
|
|||||||
boot_msg["acks"] = 0
|
boot_msg["acks"] = 0
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
await conn.sendto(boot_msg)
|
await conn.sendto(boot_msg)
|
||||||
|
break # Only send message on first connection to avoid duplicates
|
||||||
|
|
||||||
if args.message and not args.daemon:
|
if args.message and not args.daemon:
|
||||||
# Message-only mode
|
# Message-only mode
|
||||||
@@ -739,7 +741,7 @@ def main(argv=None):
|
|||||||
|
|
||||||
# Daemonize if requested
|
# Daemonize if requested
|
||||||
if args.daemon:
|
if args.daemon:
|
||||||
print("Daemonizing...")
|
logging.info("Daemonizing...")
|
||||||
daemonize()
|
daemonize()
|
||||||
_reconfigure_logging_for_daemon(log_level)
|
_reconfigure_logging_for_daemon(log_level)
|
||||||
logging.info(f"hbc starting, sending heartbeat to {', '.join(args.hosts)}")
|
logging.info(f"hbc starting, sending heartbeat to {', '.join(args.hosts)}")
|
||||||
|
|||||||
+1
-1
@@ -890,7 +890,7 @@ async def start(
|
|||||||
tmpl = env.get_template("settings.html")
|
tmpl = env.get_template("settings.html")
|
||||||
body = tmpl.render(
|
body = tmpl.render(
|
||||||
title="Settings - Heartbeat",
|
title="Settings - Heartbeat",
|
||||||
sections=settings_mod.get_settings_sections(config),
|
sections=settings_mod.get_settings_sections(config, threshold_checker=threshold_checker),
|
||||||
current_user=current_user.to_dict() if current_user else None,
|
current_user=current_user.to_dict() if current_user else None,
|
||||||
active_page="settings",
|
active_page="settings",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ async def _run_async(config, config_path=None):
|
|||||||
config=config,
|
config=config,
|
||||||
hbdclass=hbdclass,
|
hbdclass=hbdclass,
|
||||||
tcss=None,
|
tcss=None,
|
||||||
|
threshold_checker=threshold_checker,
|
||||||
verbose=config.get("verbose", False),
|
verbose=config.get("verbose", False),
|
||||||
get_now=lambda: time.time(),
|
get_now=lambda: time.time(),
|
||||||
VER="",
|
VER="",
|
||||||
|
|||||||
+30
-37
@@ -88,7 +88,7 @@ def _sanitize_channel(name, cfg):
|
|||||||
# Public API
|
# Public API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def get_settings_sections(config: dict) -> list:
|
def get_settings_sections(config: dict, threshold_checker=None) -> list:
|
||||||
"""Return ordered list of setting sections for the settings page.
|
"""Return ordered list of setting sections for the settings page.
|
||||||
|
|
||||||
Each section:
|
Each section:
|
||||||
@@ -182,46 +182,39 @@ def get_settings_sections(config: dict) -> list:
|
|||||||
})
|
})
|
||||||
|
|
||||||
# ---- Threshold configurations -----------------------------------------
|
# ---- Threshold configurations -----------------------------------------
|
||||||
def _parse_metric_row(metric_path, metric_cfg):
|
def _tc_to_row(tc):
|
||||||
if not isinstance(metric_cfg, dict):
|
|
||||||
return None
|
|
||||||
return {
|
return {
|
||||||
"metric": metric_path,
|
"metric": tc.metric_path,
|
||||||
"operator": metric_cfg.get("operator", ">"),
|
"operator": tc.operator.value,
|
||||||
"warning": metric_cfg.get("warning"),
|
"warning": tc.warning,
|
||||||
"critical": metric_cfg.get("critical"),
|
"critical": tc.critical,
|
||||||
"hysteresis": metric_cfg.get("hysteresis"),
|
"hysteresis": tc.hysteresis,
|
||||||
"count": metric_cfg.get("count", 1),
|
"count": tc.count,
|
||||||
"enabled": metric_cfg.get("enabled", True),
|
"enabled": tc.enabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
threshold_config_list = []
|
threshold_config_list = []
|
||||||
raw_tconfigs = config.get("threshold_configs") or {}
|
if threshold_checker is not None:
|
||||||
if raw_tconfigs:
|
if threshold_checker.threshold_configs:
|
||||||
for cfg_name, cfg_data in sorted(raw_tconfigs.items()):
|
for cfg_name, cfg_metrics in sorted(threshold_checker.threshold_configs.items()):
|
||||||
if not isinstance(cfg_data, dict):
|
# For the default config use the merged effective set;
|
||||||
continue
|
# for named overrides use only the explicitly defined metrics
|
||||||
metrics = [
|
# (threshold_raw_configs) so inherited defaults are not repeated.
|
||||||
r for r in (
|
if cfg_name == "default":
|
||||||
_parse_metric_row(mp, mc)
|
display_metrics = cfg_metrics
|
||||||
for mp, mc in (cfg_data.get("thresholds") or {}).items()
|
else:
|
||||||
) if r
|
display_metrics = threshold_checker.threshold_raw_configs.get(cfg_name, cfg_metrics)
|
||||||
]
|
metrics = sorted(
|
||||||
threshold_config_list.append({
|
[_tc_to_row(tc) for tc in display_metrics.values()],
|
||||||
"name": cfg_name,
|
key=lambda m: m["metric"],
|
||||||
"metrics": sorted(metrics, key=lambda m: m["metric"]),
|
)
|
||||||
})
|
threshold_config_list.append({"name": cfg_name, "metrics": metrics})
|
||||||
elif config.get("thresholds"):
|
elif threshold_checker.thresholds:
|
||||||
metrics = [
|
metrics = sorted(
|
||||||
r for r in (
|
[_tc_to_row(tc) for tc in threshold_checker.thresholds.values()],
|
||||||
_parse_metric_row(mp, mc)
|
key=lambda m: m["metric"],
|
||||||
for mp, mc in config["thresholds"].items()
|
)
|
||||||
) if r
|
threshold_config_list.append({"name": "default", "metrics": metrics})
|
||||||
]
|
|
||||||
threshold_config_list.append({
|
|
||||||
"name": "default",
|
|
||||||
"metrics": sorted(metrics, key=lambda m: m["metric"]),
|
|
||||||
})
|
|
||||||
|
|
||||||
# ---- Hosts summary ----------------------------------------------------
|
# ---- Hosts summary ----------------------------------------------------
|
||||||
hosts_list = []
|
hosts_list = []
|
||||||
|
|||||||
@@ -152,6 +152,31 @@
|
|||||||
}
|
}
|
||||||
.host-action-btn.delete-btn:hover { background: #ffcdd2; }
|
.host-action-btn.delete-btn:hover { background: #ffcdd2; }
|
||||||
|
|
||||||
|
/* ── Action result toast ───────────────────────────────────── */
|
||||||
|
#action-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
background: #323232;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
max-width: 480px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.25s, transform 0.25s;
|
||||||
|
z-index: 9000;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
#action-toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
#action-toast.error { background: #c62828; }
|
||||||
|
|
||||||
/* ── Host body ──────────────────────────────────────────────── */
|
/* ── Host body ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.host-body {
|
.host-body {
|
||||||
@@ -401,12 +426,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="os-label" id="os-label-{{ host.name }}"></span>
|
<span class="os-label" id="os-label-{{ host.name }}"></span>
|
||||||
{% if host.is_owner %}
|
{% if host.is_owner %}
|
||||||
<a class="host-action-btn update-btn"
|
<button class="host-action-btn update-btn"
|
||||||
href="/u?h={{ host.name }}"
|
onclick="event.stopPropagation(); hostAction(this, '/u?h={{ host.name }}')">Update</button>
|
||||||
onclick="event.stopPropagation()">Update</a>
|
<button class="host-action-btn delete-btn"
|
||||||
<a class="host-action-btn delete-btn"
|
onclick="event.stopPropagation(); hostDelete(this, '{{ host.name }}')">Delete</button>
|
||||||
href="/d?h={{ host.name }}"
|
|
||||||
onclick="event.stopPropagation(); return confirm('Delete host {{ host.name }}?')">Delete</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1204,6 +1227,49 @@
|
|||||||
fetchHostGlance(first.dataset.hostname);
|
fetchHostGlance(first.dataset.hostname);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// ── Host action helpers ──────────────────────────────────────
|
||||||
|
|
||||||
|
let _toastTimer = null;
|
||||||
|
function showToast(msg, isError) {
|
||||||
|
const t = document.getElementById('action-toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.classList.toggle('error', !!isError);
|
||||||
|
t.classList.add('show');
|
||||||
|
clearTimeout(_toastTimer);
|
||||||
|
_toastTimer = setTimeout(() => t.classList.remove('show'), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hostAction(btn, url) {
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const text = await res.text();
|
||||||
|
showToast(text, !res.ok);
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Request failed: ' + e.message, true);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hostDelete(btn, hostname) {
|
||||||
|
if (!confirm('Delete host ' + hostname + '?')) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/d?h=' + encodeURIComponent(hostname));
|
||||||
|
const text = await res.text();
|
||||||
|
showToast(text, !res.ok);
|
||||||
|
if (res.ok) {
|
||||||
|
const card = document.querySelector(`.host-card[data-hostname="${hostname}"]`);
|
||||||
|
if (card) card.remove();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Request failed: ' + e.message, true);
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id="action-toast"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "hbd"
|
name = "hbd"
|
||||||
version = "5.1.17"
|
version = "5.1.18"
|
||||||
description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
|
description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+4
-1
@@ -41,7 +41,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
# updated by scripts/bumpminor.sh
|
# updated by scripts/bumpminor.sh
|
||||||
__version__ = "5.1.17"
|
__version__ = "5.1.18"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Protocol (mirrors hbd/common/proto.py)
|
# Protocol (mirrors hbd/common/proto.py)
|
||||||
@@ -1054,6 +1054,7 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
|
|||||||
bmsg["msg"] = args.message
|
bmsg["msg"] = args.message
|
||||||
for c in connections:
|
for c in connections:
|
||||||
await c.sendto(bmsg)
|
await c.sendto(bmsg)
|
||||||
|
break
|
||||||
if args.message and not args.daemon:
|
if args.message and not args.daemon:
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
for c in connections:
|
for c in connections:
|
||||||
@@ -1090,6 +1091,8 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
|
|||||||
await conn.sendto({"shutdown": 1, "acks": conn.ackcount})
|
await conn.sendto({"shutdown": 1, "acks": conn.ackcount})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
break
|
||||||
|
for conn in connections:
|
||||||
conn.close()
|
conn.close()
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
|
|||||||
Reference in New Issue
Block a user