feat(ansible): a run's scope reads as the target or group it names

#5014 "Ansible run scope is shown as a raw id"

- inventory_gen.describe_scope / scope_labels turn "steward:target:<uuid>"
  into "target web-1", "steward:group:<uuid>" into "group Docker Swarm",
  and a deleted one into "target abcdef01 (deleted)".
- MCP run and schedule rows carry scope_label beside the stored scope; the
  ansible.run.started audit detail records it too.
- The web run page and schedules table show the name, with the stored
  scope as the tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 10:29:06 -04:00
co-authored by Claude Opus 5.5
parent 30cb375079
commit 06e89308a8
7 changed files with 74 additions and 12 deletions
+6 -1
View File
@@ -9,6 +9,7 @@ from quart import (
from sqlalchemy import select
from steward.ansible import executor, sources as src_module
from steward.ansible.inventory_gen import scope_labels
from steward.auth.middleware import require_role
from steward.models.ansible import AnsibleRun, AnsibleRunStatus
from steward.models.ansible_schedule import AnsibleSchedule
@@ -306,8 +307,10 @@ async def run_detail(run_id: str):
res = await db.execute(
select(User.username).where(User.id == run.triggered_by))
triggered_label = res.scalar_one_or_none() or "(deleted user)"
scope_label = (await scope_labels(db, [run.inventory_scope])).get(run.inventory_scope)
return await render_template(
"ansible/run_detail.html", run=run, triggered_label=triggered_label)
"ansible/run_detail.html", run=run, triggered_label=triggered_label,
scope_label=scope_label)
@ansible_bp.post("/runs/<run_id>/cancel")
@@ -368,6 +371,7 @@ async def schedules():
r.id: r for r in (await db.execute(
select(AnsibleRun).where(AnsibleRun.id.in_(run_ids)))).scalars().all()
} if run_ids else {}
labels = await scope_labels(db, [s.inventory_scope for s in scheds])
source_data = [
{"name": s["name"], "playbooks": src_module.discover_playbooks(s["path"])}
@@ -380,6 +384,7 @@ async def schedules():
rows.append({
"s": s,
"last_run": runs.get(s.last_run_id) if s.last_run_id else None,
"scope_label": labels.get(s.inventory_scope, s.inventory_scope),
"next_run": (s.last_run_at + timedelta(seconds=s.interval_seconds)) if s.last_run_at else None,
})
if editing_id and s.id == editing_id: