feat(server): host presence, absent state and host-level status

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M
This commit is contained in:
2026-09-21 09:25:33 -04:00
co-authored by Claude Sonnet 5
parent 1898bc52e5
commit 2f1cad9156
2 changed files with 139 additions and 1 deletions
+45 -1
View File
@@ -29,6 +29,7 @@ class Connection:
UP = "up"
DOWN = "down"
OVERDUE = "overdue"
ABSENT = "absent"
def __init__(self, host, cid, addr, afam):
self.host = host
@@ -94,7 +95,9 @@ class Connection:
now = time.time()
if not Null:
d["addr"] = self.addr
if self.rtts[-1]:
if self.state == Connection.ABSENT:
d["rtt"] = ""
elif self.rtts[-1]:
d["rtt"] = "%d" % round(self.rtts[-1])
elif self.state == Connection.UNKNOWN:
d["rtt"] = ""
@@ -299,6 +302,8 @@ class Host:
self.alert_states = {}
# Stale-data timers: {plugin_name: asyncio.TimerHandle}
self.plugin_timers = {}
# Client-reported protocol presence: {afam: {"present": bool, "ts": float}}
self.presence: dict = {}
# User access control
self.owner: str | None = None # username of owner
self.managers: list = [] # usernames with manager role
@@ -322,6 +327,7 @@ class Host:
d["name"] = "<b>%s</b>" % d["name"]
d["dyn"] = str(self.dyn)
d["num"] = self.num
d["status"] = self.status()
# Add alert counts (split by acknowledged status)
warning_unacked = 0
@@ -444,6 +450,7 @@ class Host:
_, os_data = latest_os
hbc_version = os_data.get("hbc_version")
ddict["hbc_version"] = hbc_version
ddict["status"] = self.status()
return ddict
@@ -459,6 +466,41 @@ class Host:
else:
return addr.find(".") > 0
def set_presence(self, afam, present, now):
"""Record the client's report of whether it has internet connectivity on afam."""
self.presence[afam] = {"present": bool(present), "ts": now}
def flag_present(self, afam):
"""Client-reported presence; True when the client never reported (legacy clients)."""
entry = getattr(self, "presence", {}).get(afam)
return True if entry is None else entry["present"]
def family_present(self, afam):
"""True if afam should be monitored: the client says it is present, or heartbeats
are demonstrably arriving on it (real traffic overrides a false "absent")."""
conn = self.connections.get(afam)
return self.flag_present(afam) or (conn is not None and conn.state == Connection.UP)
def status(self):
"""Host-level status: down, overdue, simplex, up or unknown.
overdue: every present family is overdue. simplex: both present, one up, one overdue
(same severity as up). Families the client reports absent are not considered.
"""
conns = self.connections
if any(c.state == Connection.DOWN for c in conns.values()):
return "down"
present = [c for afam, c in conns.items() if self.family_present(afam)]
up = sum(c.state == Connection.UP for c in present)
overdue = sum(c.state == Connection.OVERDUE for c in present)
if up and overdue:
return "simplex"
if up:
return "up"
if overdue:
return "overdue"
return "unknown"
def conndata(self, cid, addr, rtt, now):
if addr[0:7] == "::ffff:":
addr = addr[7:]
@@ -499,6 +541,8 @@ class Host:
self.monitors = []
if not hasattr(self, "plugin_timers"):
self.plugin_timers = {}
if not hasattr(self, "presence"):
self.presence = {}
pass