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:
+33
-128
@@ -20,8 +20,6 @@ import ci_orchestrator.web.app as app_module
|
||||
from ci_orchestrator.backends.errors import BackendNotAvailable
|
||||
from ci_orchestrator.backends.protocol import VmHandle
|
||||
from ci_orchestrator.config import BackendConfig, CargoCacheConfig, Config, Paths
|
||||
from ci_orchestrator.operations.cache import CacheStatus
|
||||
from ci_orchestrator.operations.host import DiskStatus, RunnerStatus
|
||||
from ci_orchestrator.operations.vms import LiveVm
|
||||
from ci_orchestrator.web.app import create_app, get_backend
|
||||
|
||||
@@ -182,7 +180,7 @@ def test_get_backend_degrades_to_none_when_vmrun_missing(
|
||||
assert "unavailable" in resp.text.lower()
|
||||
|
||||
|
||||
# ── /cache ────────────────────────────────────────────────────────────────
|
||||
# ── /cache, /runners ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cache_route_renders_status(tmp_path: Path) -> None:
|
||||
@@ -210,14 +208,9 @@ def test_cache_route_degrades_when_operations_raises(
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── /host ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_runners_route_renders_runner_status(
|
||||
def test_runners_route_renders_every_unit(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# Stage E: the /runners partial carries only the runner statuses -- the
|
||||
# disk card was folded into the status-header Disk tile.
|
||||
_quiet_host(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
client = TestClient(app)
|
||||
@@ -230,9 +223,10 @@ def test_runners_route_renders_runner_status(
|
||||
assert "act-runner-rust.service" in resp.text
|
||||
|
||||
|
||||
def test_runners_route_degrades_when_disk_status_raises(
|
||||
def test_host_route_degrades_when_disk_status_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Stage F put the disk gauge in the Host card, so this is its degrade path."""
|
||||
def _boom(_path: Path) -> Any:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
@@ -241,9 +235,10 @@ def test_runners_route_degrades_when_disk_status_raises(
|
||||
app = create_app(_make_config(tmp_path))
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/runners")
|
||||
resp = client.get("/host")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "unknown" in resp.text
|
||||
|
||||
|
||||
def test_runners_route_degrades_when_runner_status_raises(
|
||||
@@ -274,131 +269,22 @@ def test_static_mount_serves_app_css(tmp_path: Path) -> None:
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── _health_summary (A3) ─────────────────────────────────────────────────
|
||||
# ── dashboard composition (Stage F) ──────────────────────────────────────
|
||||
|
||||
_ACTIVE_RUNNER = RunnerStatus(name="act-runner.service", active=True, sub_state="running")
|
||||
_DEAD_RUNNER = RunnerStatus(name="act-runner-rust.service", active=False, sub_state="dead")
|
||||
_LIVE_VM = LiveVm(
|
||||
name="job1", vmx="/x/job1.vmx", ip="10.0.0.5", guest_os="ubuntu-64",
|
||||
uptime_sec=120, is_orphan=False,
|
||||
)
|
||||
_ORPHAN_VM = LiveVm(
|
||||
name="job2", vmx="/x/job2.vmx", ip=None, guest_os="ubuntu-64",
|
||||
uptime_sec=99999, is_orphan=True,
|
||||
)
|
||||
_CACHE = CacheStatus(
|
||||
root=Path("/cache"), total_bytes=1024, entry_count=1, oldest_age_days=1, retention_days=30
|
||||
)
|
||||
|
||||
|
||||
def test_health_summary_backend_unavailable_is_vm_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=False, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_class == "tile-unknown"
|
||||
assert health.vm_label == "unknown"
|
||||
|
||||
|
||||
def test_health_summary_orphan_present_is_vm_warn() -> None:
|
||||
health = app_module._health_summary(
|
||||
[_LIVE_VM, _ORPHAN_VM], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_count == 2
|
||||
assert health.orphan_count == 1
|
||||
assert health.vm_class == "tile-warn"
|
||||
assert health.vm_label == "orphaned"
|
||||
|
||||
|
||||
def test_health_summary_no_orphans_is_vm_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[_LIVE_VM], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_class == "tile-ok"
|
||||
assert health.vm_label == "ok"
|
||||
|
||||
|
||||
def test_health_summary_no_runners_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.runners_total == 0
|
||||
assert health.runner_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_all_runners_active_is_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[_ACTIVE_RUNNER]
|
||||
)
|
||||
assert health.runners_active == 1
|
||||
assert health.runners_total == 1
|
||||
assert health.runner_class == "tile-ok"
|
||||
|
||||
|
||||
def test_health_summary_a_down_runner_is_bad() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None,
|
||||
runners=[_ACTIVE_RUNNER, _DEAD_RUNNER],
|
||||
)
|
||||
assert health.runners_active == 1
|
||||
assert health.runners_total == 2
|
||||
assert health.runner_class == "tile-bad"
|
||||
assert health.runner_label == "down"
|
||||
|
||||
|
||||
def test_health_summary_disk_none_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.disk_used_pct is None
|
||||
assert health.disk_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_disk_above_critical_is_bad() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=5.0, used_pct=90.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-bad"
|
||||
assert health.disk_label == "critical"
|
||||
|
||||
|
||||
def test_health_summary_disk_above_elevated_is_warn() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=25.0, used_pct=75.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-warn"
|
||||
assert health.disk_label == "elevated"
|
||||
|
||||
|
||||
def test_health_summary_disk_healthy_is_ok() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=60.0, used_pct=40.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-ok"
|
||||
assert health.disk_label == "ok"
|
||||
|
||||
|
||||
def test_health_summary_cache_none_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.cache_bytes is None
|
||||
assert health.cache_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_cache_present_is_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=_CACHE, disk=None, runners=[]
|
||||
)
|
||||
assert health.cache_bytes == 1024
|
||||
assert health.cache_class == "tile-ok"
|
||||
|
||||
|
||||
def test_dashboard_includes_status_header_tiles(
|
||||
def test_dashboard_renders_the_info_cards_without_a_status_header(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Stage F: the tiles are gone; every readout they carried is in a card.
|
||||
|
||||
Threshold levels themselves are covered in ``test_web_cards.py``
|
||||
(``_resource_levels``) -- this only pins the page composition.
|
||||
"""
|
||||
_quiet_host(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
@@ -407,4 +293,23 @@ def test_dashboard_includes_status_header_tiles(
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert 'id="status-header"' in resp.text
|
||||
assert 'id="status-header"' not in resp.text
|
||||
assert 'id="host-card"' in resp.text
|
||||
|
||||
|
||||
def test_dashboard_lists_a_running_vm_under_its_run(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The VM count left with the tiles -- the Executions card is where VMs show."""
|
||||
_quiet_host(monkeypatch)
|
||||
vmx = tmp_path / "build-vms" / "job1" / "job1.vmx"
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend(
|
||||
[VmHandle(identifier=str(vmx), name=_LIVE_VM.name)]
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "job1" in resp.text
|
||||
|
||||
Reference in New Issue
Block a user