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:
@@ -1,4 +1,5 @@
|
||||
"""The host page: what sits where (#5214)."""
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
|
||||
from quart import render_template, session
|
||||
@@ -52,3 +53,51 @@ async def test_the_agent_card_keeps_its_actions_under_manage(app):
|
||||
for action in (">Update agent</button>", ">Rotate token</button>", ">Unregister agent</button>"):
|
||||
assert page.index(action) > manage
|
||||
assert ">Remove</button>" not in page
|
||||
|
||||
|
||||
async def test_the_hosts_list_says_what_is_wrong_and_labels_its_figures(app):
|
||||
"""#5279–#5281: labelled CPU and Memory, one uptime figure, status with how
|
||||
long, no SLA tab and no per-row edit/delete."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
down = SimpleNamespace(id="h1", name="nas", address="10.0.0.5")
|
||||
quiet = SimpleNamespace(id="h2", name="pi", address="")
|
||||
async with app.test_request_context("/hosts/"):
|
||||
session["user_role"] = "admin"
|
||||
page = await render_template(
|
||||
"hosts/list.html", hosts=[down, quiet],
|
||||
host_status={"h1": {"state": "down", "latency_ms": None, "count": 2,
|
||||
"down_since": now - timedelta(minutes=12)}},
|
||||
uptime={"h1": {"24h": 91.5, "7d": 98.0, "30d": 99.42}},
|
||||
agent={"pi": {"fresh": False, "last_at": now - timedelta(minutes=7),
|
||||
"cpu_pct": 3.0, "mem_used_pct": 41.0}})
|
||||
assert re.findall(r"<th[^>]*>([^<]+)</th>", page) == [
|
||||
"Name", "Address", "Status", "Latency", "Monitors", "30d uptime", "CPU", "Memory"]
|
||||
assert "for 12m" in page and "agent silent" in page and "for 7m" in page
|
||||
assert "99.42%" in page and 'title="24h 91.50% · 7d 98.00%"' in page
|
||||
assert ">41%<" in page and "tone-dim" in page # a silent agent's last reading, greyed
|
||||
assert "/hosts/uptime" not in page and ">Delete<" not in page and ">Edit<" not in page
|
||||
|
||||
|
||||
async def test_a_host_without_an_agent_can_get_a_manual_install(app):
|
||||
"""#5282: the manual install moved here from the fleet's add-host form."""
|
||||
from tests._templates import render_plugin_template
|
||||
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
|
||||
async with app.test_request_context("/plugins/host_agent/panel/h1"):
|
||||
session["user_role"] = "admin"
|
||||
page = await render_plugin_template(
|
||||
app, "host_agent", "panel.html", host=host, reg=None, reporting=False, stale=False,
|
||||
target=None, ansible_available=False, managed_key_set=False)
|
||||
assert 'action="/plugins/host_agent/fleet/h1/register"' in page and ">Install manually</button>" in page
|
||||
|
||||
|
||||
async def test_add_host_sends_a_clash_back_with_what_was_typed(app):
|
||||
"""#5283: an inline error and the typed values, not a bare text page."""
|
||||
async with app.test_request_context("/hosts/new"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template("hosts/form.html", host=None, error="A host named nas already exists.",
|
||||
values={"name": "nas", "address": "10.0.0.9"})
|
||||
assert 'role="alert">A host named nas already exists.<' in page
|
||||
assert 'value="nas"' in page and 'value="10.0.0.9"' in page
|
||||
assert re.search(r'<input[^>]*name="address"[^>]*>', page).group(0).count("required") == 0
|
||||
assert '<a href="/hosts/">Hosts</a>' in page and ">Add host</span>" in page
|
||||
|
||||
@@ -44,12 +44,3 @@ def test_a_host_page_shows_only_the_alerts_on_that_host():
|
||||
assert not watches_host(rule("docker", "nas2/plex"), "nas", mine)
|
||||
assert watches_host(rule("http", "nas web"), "nas", mine)
|
||||
assert not watches_host(rule("http", "other web"), "nas", mine)
|
||||
|
||||
|
||||
def test_uptime_lists_the_worst_first_and_the_unmeasured_last():
|
||||
from steward.hosts.service import worst_uptime_first
|
||||
hosts = [NS(id=i, name=n) for i, n in [("a", "alpha"), ("b", "bravo"), ("c", "charlie"), ("d", "delta")]]
|
||||
uptime = {"a": {"24h": 100.0, "7d": 99.0, "30d": 99.5},
|
||||
"b": {"24h": 92.0, "7d": 98.0, "30d": 99.0},
|
||||
"c": {"24h": 100.0, "7d": 95.0, "30d": 97.0}}
|
||||
assert [h.name for h in worst_uptime_first(hosts, uptime)] == ["bravo", "charlie", "alpha", "delta"]
|
||||
|
||||
@@ -125,3 +125,5 @@ def test_last_results_are_the_newest_n_oldest_first_and_host_status_reads_the_la
|
||||
assert len(last[b]) == 1 and last[quiet] == []
|
||||
assert status[hid]["state"] == "down" and status[hid]["count"] == 2
|
||||
assert status[hid]["latency_ms"] == 12.0
|
||||
# Down since a's first failure after its last success (#5281).
|
||||
assert abs((status[hid]["down_since"] - (now - timedelta(minutes=1))).total_seconds()) < 1
|
||||
|
||||
@@ -31,9 +31,10 @@ async def test_silent_agents_come_first_and_the_table_leads_the_page(app):
|
||||
new_token=None, install_url=None, ansible_available=False,
|
||||
deploy_targets=[], deploy_groups=[], managed_key_set=False)
|
||||
|
||||
assert page.index("<th>Status</th>") < page.index('action="/plugins/host_agent/fleet/add-host"')
|
||||
# Hosts are added in one place, Hosts → Add host (#5282).
|
||||
assert "fleet/add-host" not in page and 'href="/hosts/new"' in page
|
||||
assert '<span class="badge badge-warn">Silent</span>' in page
|
||||
assert '<span class="badge badge-warn">Never reported</span>' in page
|
||||
assert '<span class="badge badge-ok">Reporting</span>' in page
|
||||
assert page.index('href="/hosts/h2">nas</a>') < page.index('href="/hosts/h1">web</a>')
|
||||
assert 'data-confirm="Delete the agent registration for nas?' in page
|
||||
assert 'data-confirm="Unregister the agent for nas?' in page
|
||||
|
||||
Reference in New Issue
Block a user