feat(ansible): schedules read as sentences, each with its own page (#5332)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 59s
CI / unit (push) Failing after 1m22s
CI / publish (push) Skipped

- The Schedules tab: a verdict line (failing first, paused, next due),
  each schedule as one line (what runs where, how often, when next, how it
  last went); the row opens its page; Run now and Pause stay in the row.
- /ansible/schedules/new and /ansible/schedules/<id>: the form on classes,
  dry run beside the button, the playbook's variables rendered with the
  page; at the foot Delete behind the confirm dialog. ?source=&playbook=
  start a new one on a playbook (Schedule it on the playbook's page).
- A run started by a schedule links to that schedule's page.
- The run page's id guard allows the Run again link's ?from=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 23:51:15 -04:00
co-authored by Claude Opus 5.5
parent 72a3261582
commit 0c72892f58
11 changed files with 348 additions and 211 deletions
+6 -5
View File
@@ -102,14 +102,14 @@ async def test_the_runs_page_opens_on_its_verdict(app):
now = datetime.now(timezone.utc)
failed = run_view(_run("ffff0000", AnsibleRunStatus.failed, 90, took_s=5), {"steward:target:t1": "target web-1"}, now)
calm = {"days": 30, "total": 33, "failed": 0, "running": 0, "latest_failed": None,
"next": {"name": "Nightly prune", "at": now + timedelta(hours=2, minutes=5)}}
"next": {"id": "s1", "name": "Nightly prune", "at": now + timedelta(hours=2, minutes=5)}}
bad = {"days": 30, "total": 33, "failed": 2, "running": 1, "latest_failed": failed, "next": None}
async with app.test_request_context("/ansible/"):
session["user_role"] = "viewer"
ok = await render_template("ansible/index.html", **_ctx([], calm))
worse = await render_template("ansible/index.html", **_ctx([], bad, f={"q": "x", "status": "", "origin": ""}))
assert '<span class="tone-ok">33 runs in the last 30 days, all succeeded</span>' in ok
assert 'next: <a href="/ansible/schedules">Nightly prune</a> <time' in ok and ">in 2h" in ok
assert 'next: <a href="/ansible/schedules/s1">Nightly prune</a> <time' in ok and ">in 2h" in ok
assert '<span class="tone-crit">2 of 33 runs failed</span> in the last 30 days' in worse
assert 'latest <a href="/ansible/runs/ffff0000">update.yml on web-1</a>' in worse
assert '<span class="tone-info">1 running now</span>' in worse
@@ -126,12 +126,12 @@ def test_the_next_schedule_is_the_soonest_enabled_one():
now = datetime.now(timezone.utc)
def sched(name, ran_min_ago, every_s, enabled=True):
return SimpleNamespace(name=name, enabled=enabled, interval_seconds=every_s,
return SimpleNamespace(id=f"id-{name}", name=name, enabled=enabled, interval_seconds=every_s,
last_run_at=now - timedelta(minutes=ran_min_ago) if ran_min_ago is not None else None)
daily, hourly = sched("daily", 60, 86400), sched("hourly", 30, 3600)
assert next_scheduled([daily, hourly], now)["name"] == "hourly"
assert next_scheduled([daily, sched("paused", 59, 3600, enabled=False)], now)["name"] == "daily"
assert next_scheduled([daily, sched("new", None, 3600)], now) == {"name": "new", "at": None} # due now
assert next_scheduled([daily, sched("new", None, 3600)], now) == {"id": "id-new", "name": "new", "at": None} # due now
assert next_scheduled([], now) is None
@@ -176,7 +176,8 @@ async def test_run_detail_titles_the_run_and_keeps_the_trail(app):
assert '<span class="badge badge-warn">Interrupted</span>' in verdict and " in 10m" in verdict
assert 'by <a href="/ansible/schedules">schedule nightly</a>' in verdict
assert "/cancel" not in page # an interrupted run has nothing to cancel
assert "dddd4444" not in page.replace("/ansible/runs/dddd4444", "")
# No run id as text: only in the links to the run and to run it again.
assert "dddd4444" not in page.replace("/ansible/runs/dddd4444", "").replace("?from=dddd4444#", "")
# Each host's result in words, the failed host first.
assert page.index(">zulu<") < page.index(">alpha<")
assert '<span class="tone-crit">1 failed</span> · 1 ok' in page and ">3 ok<" in page