feat(web): dashboard redesign — Gitea palette, health panels, VM/audit enrichment (stages A–C)
Stage A: swap CSS tokens to the real Gitea dark palette (accent blue #4183c4); add Jinja formatting filters (humanbytes/duration/relative_time/localtime/shorten_path/thousands), a health status-header, and an inline SVG icon sprite; fix two UI bugs — unthemed action pages (htmx swap into #action-slot with a full-page HX-Request fallback) and polling wiping form input (hx-preserve). Stage B: four read-only panels that degrade to notices (never 500) — host resources (/proc, two-sample CPU%), VMware module health, template inventory (.vmem warn + last backup), and a path-contained file browser; backup card becomes a validated template dropdown; expensive dir-size walk is TTL-cached. Adds operations/inventory.py plus host_resources()/vmware_module_health() in operations/host.py. Stage C: parse_clone_name() adds a Job column (job_id + started) to the VM table; nav gains a Gitea link-out derived from the OIDC issuer origin (no fabricated run URLs); audit page gets action icons, relative time, and a client-side filter. Gates: 989 tests pass, coverage 94.12%, ruff + mypy --strict clean. Stage D (service management) intentionally deferred.
This commit is contained in:
@@ -20,6 +20,9 @@ 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
|
||||
|
||||
_DiskUsage = namedtuple("_DiskUsage", ["total", "used", "free"])
|
||||
@@ -268,3 +271,139 @@ def test_static_mount_serves_app_css(tmp_path: Path) -> None:
|
||||
resp = client.get("/static/app.css")
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── _health_summary (A3) ─────────────────────────────────────────────────
|
||||
|
||||
_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(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_quiet_host(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert 'id="status-header"' in resp.text
|
||||
|
||||
Reference in New Issue
Block a user