feat(ansible): runs named by what ran where, with ago, duration and one status badge (#5125)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 46s
CI / unit (push) Successful in 1m7s
CI / publish (push) Successful in 59s

Runs read "update.yml on web-1" (run_title over the scope label, beside
describe_scope) on the runs list, the run page's title and breadcrumb, and
the run-started panel. The list shows started |ago (full time on hover),
|duration (counting "so far" while a run is going) and a dry-run tag.

Run statuses join STATES, so the list and the run page use status_badge:
queued dim, running info (blue, so work in progress doesn't read as a
problem), success ok "Succeeded", failed crit, interrupted warn, cancelled
dim. run_started.html leaves its hard-coded colours for tokens; the run
page's facts move to the .kv list and its failures to var(--red). Template
hex colours: 54 -> 47.

Also fixes #5124's sidebar count, which CI run 8389 failed on: the stylesheet
guard allows no weight above 500, and .nav-count asked for 600.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 18:16:07 -04:00
co-authored by Claude Opus 5.5
parent 4fbe93bdf7
commit c2a79c8bb0
9 changed files with 194 additions and 79 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Ansible runs are named by what ran where, say how long ago and for how
long, and wear the one status badge every page uses (#5125)."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
import pytest
from quart import render_template, session
from steward.ansible.inventory_gen import run_title
from steward.ansible.routes import run_duration, run_view
from steward.models.ansible import AnsibleRunStatus
from tests.test_style_tokens import hex_colours
@pytest.mark.parametrize("label, title", [
("target web-1", "update.yml on web-1"),
("group Docker Swarm", "update.yml on group Docker Swarm"),
("all inventory targets", "update.yml on all inventory targets"),
("homelab/hosts.ini", "update.yml on homelab/hosts.ini"),
("", "update.yml"),
])
def test_a_run_is_named_by_what_ran_where(label, title):
assert run_title("update.yml", label) == title
def _run(rid, status, started_min_ago, took_s=None, scope="steward:target:t1", check=False):
now = datetime.now(timezone.utc)
started = now - timedelta(minutes=started_min_ago)
return SimpleNamespace(
id=rid, playbook_path="update.yml", source_name="builtin", inventory_scope=scope,
status=status, started_at=started,
finished_at=started + timedelta(seconds=took_s) if took_s is not None else None,
params={"check": True} if check else None, results=None, output="")
def test_a_running_run_counts_its_duration_so_far():
now = datetime.now(timezone.utc)
run = _run("r", AnsibleRunStatus.running, 3)
assert run_duration(run, now) == pytest.approx(180, abs=2)
assert run_duration(_run("f", AnsibleRunStatus.success, 10, took_s=95), now) == 95
async def test_the_runs_list_names_each_run_with_one_badge(app):
now = datetime.now(timezone.utc)
labels = {"steward:target:t1": "target web-1", "steward:all": "all inventory targets"}
runs = [run_view(r, labels, now) for r in (
_run("aaaa1111", AnsibleRunStatus.running, 2),
_run("bbbb2222", AnsibleRunStatus.failed, 60, took_s=125, scope="steward:all", check=True),
_run("cccc3333", AnsibleRunStatus.success, 120, took_s=40),
)]
async with app.test_request_context("/ansible/"):
session["user_role"] = "viewer"
page = await render_template("ansible/index.html", runs=runs)
main = page[page.index("<main"):]
assert '<a href="/ansible/runs/aaaa1111" title="steward:target:t1">update.yml on web-1</a>' in main
assert ">update.yml on all inventory targets</a>" in main
assert '<span class="badge badge-info">Running</span>' in main
assert '<span class="badge badge-crit">Failed</span>' in main
assert '<span class="badge badge-ok">Succeeded</span>' in main
assert "2m 5s" in main and "40s" in main and " so far" in main
assert "dry run" in main and "ago" in main
assert "RUNNING" not in main and "FAILED" not in main # the old shouted status text is gone
assert not hex_colours(main), hex_colours(main)
async def test_run_detail_titles_the_run_and_keeps_the_trail(app):
now = datetime.now(timezone.utc)
run = _run("dddd4444", AnsibleRunStatus.interrupted, 30, took_s=600)
async with app.test_request_context("/ansible/runs/dddd4444"):
session["user_role"] = "operator"
page = await render_template("ansible/run_detail.html", run=run, triggered_label="system",
row=run_view(run, {"steward:target:t1": "target web-1"}, now))
assert "<title>update.yml on web-1 — Steward</title>" in page
assert 'href="/ansible/"' in page # breadcrumb back to the runs
assert '<span class="badge badge-warn">Interrupted</span>' in page
assert "<dd>10m</dd>" in page
assert "/cancel" not in page # an interrupted run has nothing to cancel
main = page[page.index("<main"):]
assert not hex_colours(main), hex_colours(main)
async def test_run_started_is_on_the_palette(app):
run = _run("eeee5555", AnsibleRunStatus.running, 0)
async with app.test_request_context("/ansible/runs"):
page = await render_template("ansible/run_started.html", run=run, title="update.yml on web-1")
assert '<span class="badge badge-info">Running</span>' in page
assert "update.yml on web-1" in page and 'href="/ansible/runs/eeee5555"' in page
assert not hex_colours(page), hex_colours(page)