feat(web): keep the four info cards, enrich them, drop the status-header tiles

The dashboard used to open with six status-header tiles *and* four compact
cards restating them (runner state lived in three places at once). The tiles
are gone; each card is now the single owner of its subject, enriched with
what only the tiles had (CPU%/RAM/disk with gauges) plus what neither had
(hostname, kernel, full CPU model, the vmware-host.service row).

All four cards share the same <dl> grammar and font scale now -- VMware and
Runners were a bordered <table> at a different size/case, which read as a
visibly different style sitting next to Host and Cargo cache's plain
label/value lists.

operations/host.py also gains delete_template()/TemplateDeleteResult, used
by the template-delete route in the next commit.
This commit is contained in:
2026-07-25 19:33:19 +02:00
parent 61c4972a88
commit c598d0658e
11 changed files with 721 additions and 459 deletions
+79
View File
@@ -40,6 +40,19 @@ _MEMINFO = (
_UPTIME = "123456.78 987654.32\n"
_LOADAVG = "0.50 0.75 1.00 3/456 7890\n"
# Two cores' worth of /proc/cpuinfo -- the parser takes the FIRST "model name".
_CPUINFO = (
"processor\t: 0\n"
"vendor_id\t: GenuineIntel\n"
"model name\t: Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz\n"
"cache size\t: 36864 KB\n"
"\n"
"processor\t: 1\n"
"model name\t: Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz\n"
)
_OSRELEASE = "7.0.0-28-generic\n"
_HOSTNAME = "ci-host\n"
def _fake_reader(calls: dict[str, int]) -> Any:
def _read(path: str) -> str:
@@ -52,6 +65,12 @@ def _fake_reader(calls: dict[str, int]) -> Any:
return _UPTIME
if path == "/proc/loadavg":
return _LOADAVG
if path == "/proc/cpuinfo":
return _CPUINFO
if path == "/proc/sys/kernel/osrelease":
return _OSRELEASE
if path == "/proc/sys/kernel/hostname":
return _HOSTNAME
raise AssertionError(f"unexpected proc path: {path}")
return _read
@@ -113,6 +132,61 @@ def test_host_resources_uptime_and_loadavg(monkeypatch: pytest.MonkeyPatch) -> N
assert result.load15 == pytest.approx(1.00)
# ── host_resources: static identity fields (CPU model, kernel, hostname) ─────
def test_host_resources_reports_cpu_model_kernel_and_hostname(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_install_fake_proc(monkeypatch)
result = host_resources()
assert result.cpu_model == "Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz"
assert result.kernel == "7.0.0-28-generic"
assert result.hostname == "ci-host"
def test_host_resources_cpu_model_is_empty_when_cpuinfo_has_no_model_name(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Not every architecture prints ``model name`` (e.g. arm64) -- degrade, don't crash."""
calls: dict[str, int] = {}
inner = _fake_reader(calls)
def _read(path: str) -> str:
if path == "/proc/cpuinfo":
return "processor\t: 0\nBogoMIPS\t: 50.00\n"
return str(inner(path))
monkeypatch.setattr(host_module, "_read_proc", _read)
monkeypatch.setattr(host_module.time, "sleep", lambda _s: None)
assert host_resources().cpu_model == ""
def test_host_resources_unreadable_static_fields_do_not_zero_live_metrics(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A missing ``/proc/cpuinfo`` must not cost us CPU%/RAM/load as well."""
calls: dict[str, int] = {}
inner = _fake_reader(calls)
def _read(path: str) -> str:
if path in ("/proc/cpuinfo", "/proc/sys/kernel/osrelease", "/proc/sys/kernel/hostname"):
raise OSError("gone")
return str(inner(path))
monkeypatch.setattr(host_module, "_read_proc", _read)
monkeypatch.setattr(host_module.time, "sleep", lambda _s: None)
result = host_resources()
assert result.cpu_pct == pytest.approx(50.0)
assert result.load1 == pytest.approx(0.50)
assert (result.cpu_model, result.kernel, result.hostname) == ("", "", "")
# ── host_resources: degrade to zeros on OSError, never raise ────────────────
@@ -134,6 +208,9 @@ def test_host_resources_degrades_to_zeros_on_oserror(monkeypatch: pytest.MonkeyP
load1=0.0,
load5=0.0,
load15=0.0,
cpu_model="",
kernel="",
hostname="",
)
@@ -144,6 +221,8 @@ def test_host_resources_degrades_to_zeros_when_second_stat_sample_fails(
calls = {"n": 0}
def _flaky(path: str) -> str:
if path.startswith("/proc/sys/kernel/") or path == "/proc/cpuinfo":
return "" # static identity reads happen before the live-metric block
if path != "/proc/stat":
raise AssertionError("should not reach meminfo after stat blows up")
calls["n"] += 1