feat(hosts): the Hosts list labels its figures and says how long; SLA tab gone; one place to add a host (#5279–#5283)
Hosts list: - CPU and Memory are their own labelled columns (#5279); a silent agent's last readings are greyed, and Status says "agent silent for 7m". - One uptime figure, 30 days, with 24h and 7d on hover. The SLA tab (/hosts/uptime) repeated it and is removed (#5280; one home per fact). - Status is a word with how long ("Down for 12m"); host_status now carries down_since. Edit and Delete live on the host's page only (#5281). - Figures are right-aligned with tabular digits (.table .num), not a monospace face. Agent fleet (#5282): its add-host form is gone, so hosts are added only on Hosts -> Add host. That form was also the only way to get a token for a manual curl install, so that moves to the host's Agent card as "Install manually" (POST /fleet/<host_id>/register), shown on the fleet page as before. "Delete" there reads "Unregister". Add host (#5283): laid out like the add-monitor form; a name clash comes back inline with what was typed instead of a bare text 400; the address is optional, as the host page already allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-12
@@ -112,7 +112,8 @@ async def create_host():
|
||||
try:
|
||||
await host_svc.check_name_free(db, host.name)
|
||||
except ValueError as exc:
|
||||
return str(exc), 400
|
||||
return await render_template("hosts/form.html", host=None, values=form,
|
||||
error=str(exc)), 400
|
||||
db.add(host)
|
||||
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
|
||||
"host.created", entity_type="host", entity_id=host.name,
|
||||
@@ -150,7 +151,8 @@ async def update_host(host_id: str):
|
||||
try:
|
||||
await host_svc.check_name_free(db, name, exclude_id=host.id)
|
||||
except ValueError as exc:
|
||||
return str(exc), 400
|
||||
return await render_template("hosts/form.html", host=host, values=form,
|
||||
error=str(exc)), 400
|
||||
host.name = name
|
||||
host.address = form["address"].strip()
|
||||
# Agent and container metrics, and the rules and windows on them,
|
||||
@@ -296,13 +298,3 @@ async def delete_host(host_id: str):
|
||||
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
|
||||
"host.deleted", entity_type="host", entity_id=host_name)
|
||||
return redirect(url_for("hosts.list_hosts"))
|
||||
|
||||
|
||||
@hosts_bp.get("/uptime")
|
||||
@require_role(UserRole.viewer)
|
||||
async def uptime_page():
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
result = await db.execute(select(Host).order_by(Host.name))
|
||||
uptime = await host_svc.compute_uptime(db)
|
||||
hosts = host_svc.worst_uptime_first(result.scalars().all(), uptime)
|
||||
return await render_template("hosts/uptime.html", hosts=hosts, uptime=uptime)
|
||||
|
||||
+15
-16
@@ -88,8 +88,10 @@ async def host_status(db, host_ids: list[str]) -> dict[str, dict]:
|
||||
"""Per-host status rollup from the latest result of each linked monitor.
|
||||
|
||||
{host_id: {"state": "up"|"down"|"pending", "latency_ms": float|None,
|
||||
"count": int}}. State is down if ANY linked monitor's latest
|
||||
result is down (worst-case), up if at least one is up, else pending.
|
||||
"count": int, "down_since": datetime|None}}. State is down if
|
||||
ANY linked monitor's latest result is down (worst-case), up if at least
|
||||
one is up, else pending. down_since is when the longest-down of its down
|
||||
monitors went down.
|
||||
"""
|
||||
if not host_ids:
|
||||
return {}
|
||||
@@ -98,32 +100,29 @@ async def host_status(db, host_ids: list[str]) -> dict[str, dict]:
|
||||
.where(Monitor.host_id.in_(host_ids), Monitor.enabled.is_(True))
|
||||
)).all()
|
||||
latest = await _last_n_results(db, [m.id for m in monitors], n=1)
|
||||
rows = [(latest[mid][-1], host_id) for mid, host_id in monitors if latest.get(mid)]
|
||||
rows = [(latest[mid][-1], host_id, mid) for mid, host_id in monitors if latest.get(mid)]
|
||||
out: dict[str, dict] = {}
|
||||
for res, host_id in rows:
|
||||
d = out.setdefault(host_id, {"state": "pending", "latency_ms": None, "count": 0})
|
||||
down: dict[str, list[str]] = {}
|
||||
for res, host_id, mid in rows:
|
||||
d = out.setdefault(host_id, {"state": "pending", "latency_ms": None, "count": 0,
|
||||
"down_since": None})
|
||||
d["count"] += 1
|
||||
if not res.is_up:
|
||||
d["state"] = "down"
|
||||
down.setdefault(host_id, []).append(mid)
|
||||
elif d["state"] != "down":
|
||||
d["state"] = "up"
|
||||
if res.is_up and res.response_ms is not None:
|
||||
d["latency_ms"] = (res.response_ms if d["latency_ms"] is None
|
||||
else min(d["latency_ms"], res.response_ms))
|
||||
if down:
|
||||
from steward.monitors.service import down_since
|
||||
for host_id, mids in down.items():
|
||||
starts = [t for t in [await down_since(db, mid) for mid in mids] if t]
|
||||
out[host_id]["down_since"] = min(starts) if starts else None
|
||||
return out
|
||||
|
||||
|
||||
def worst_uptime_first(hosts, uptime: dict) -> list:
|
||||
"""Hosts by their 24h uptime, worst first, then 7d and 30d to break ties;
|
||||
hosts with no uptime yet (no monitors) last, by name."""
|
||||
def key(h):
|
||||
u = uptime.get(h.id) or {}
|
||||
windows = [u.get(w) for w in ("24h", "7d", "30d")]
|
||||
known = [w for w in windows if w is not None]
|
||||
return (not known, *[101.0 if w is None else w for w in windows], h.name.lower())
|
||||
return sorted(hosts, key=key)
|
||||
|
||||
|
||||
def host_rank(status: dict | None, agent: dict | None) -> int:
|
||||
"""Where a host sorts in the Hosts list: a down monitor first, then an
|
||||
agent gone quiet, then monitors not yet checked, then the healthy; a host
|
||||
|
||||
Reference in New Issue
Block a user