feat(ui): each run says who or what started it; run detail without ids (#5212)
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon", batch 2: - A run records where it came from: AnsibleRun.origin ("schedule", "alert" or "mcp") and origin_id, set by the scheduler, Run now, alert actions and the MCP tools. Migration 0031 backfills past runs from alert events, schedules' last runs, and MCP audit entries. - The runs list has a Started by column: "schedule nightly", "alert rule nas down", "alice via MCP" or "alice", linked where there is a page. The raw scope tooltip is gone. - Run detail: no run id in the subtitle or body, Started by in words, and the host summary lists failed and unreachable hosts first. The live output only follows new lines while the reader is at the bottom, and when the run finishes the page reloads once so the badge and summary are final. - list_ansible_runs and get_ansible_run report origin and origin_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+17
-10
@@ -10,12 +10,13 @@ from sqlalchemy import select
|
||||
|
||||
from steward.ansible import executor, guards, sources as src_module
|
||||
from steward.ansible.inventory_gen import run_title, scope_labels
|
||||
from steward.ansible.origins import run_origins
|
||||
from steward.auth.middleware import require_role
|
||||
from steward.core.audit import audit_request as _audit
|
||||
from steward.core.paging import keyset, next_page
|
||||
from steward.models.ansible import AnsibleRun, AnsibleRunStatus
|
||||
from steward.models.ansible_schedule import AnsibleSchedule
|
||||
from steward.models.users import User, UserRole
|
||||
from steward.models.users import UserRole
|
||||
|
||||
INTERVAL_PRESETS = [
|
||||
(300, "Every 5 minutes"),
|
||||
@@ -45,8 +46,10 @@ async def index():
|
||||
runs, after = next_page((await db.execute(stmt)).scalars().all(),
|
||||
lambda r: (r.started_at, r.id))
|
||||
labels = await scope_labels(db, [r.inventory_scope for r in runs])
|
||||
origins = await run_origins(db, runs)
|
||||
now = datetime.now(timezone.utc)
|
||||
ctx = {"runs": [run_view(r, labels, now) for r in runs], "after": after}
|
||||
ctx = {"runs": [{**run_view(r, labels, now), "origin": origins[r.id]} for r in runs],
|
||||
"after": after}
|
||||
if before:
|
||||
return await render_template("ansible/_runs_page.html", **ctx)
|
||||
return await render_template("ansible/index.html", **ctx)
|
||||
@@ -359,18 +362,22 @@ async def run_detail(run_id: str):
|
||||
run = result.scalar_one_or_none()
|
||||
if run is None:
|
||||
return "Not found", 404
|
||||
# NULL triggered_by = an automated (system) run; otherwise resolve the user.
|
||||
triggered_label = "system"
|
||||
if run.triggered_by:
|
||||
res = await db.execute(
|
||||
select(User.username).where(User.id == run.triggered_by))
|
||||
triggered_label = res.scalar_one_or_none() or "(deleted user)"
|
||||
origin = (await run_origins(db, [run]))[run.id]
|
||||
labels = await scope_labels(db, [run.inventory_scope])
|
||||
return await render_template(
|
||||
"ansible/run_detail.html", run=run, triggered_label=triggered_label,
|
||||
"ansible/run_detail.html", run=run, origin=origin, hosts=hosts_worst_first(run.results),
|
||||
row=run_view(run, labels, datetime.now(timezone.utc)))
|
||||
|
||||
|
||||
def hosts_worst_first(results: dict | None) -> list[tuple[str, dict]]:
|
||||
"""A run's per-host recap, failed or unreachable hosts first, then those
|
||||
that changed something, then the rest, each by name."""
|
||||
hosts = (results or {}).get("hosts") or {}
|
||||
return sorted(hosts.items(), key=lambda kv: (
|
||||
-((kv[1].get("failed") or 0) + (kv[1].get("unreachable") or 0) > 0),
|
||||
-((kv[1].get("changed") or 0) > 0), kv[0]))
|
||||
|
||||
|
||||
@ansible_bp.post("/runs/<run_id>/cancel")
|
||||
@require_role(UserRole.operator)
|
||||
async def cancel_run(run_id: str):
|
||||
@@ -614,7 +621,7 @@ async def run_schedule_now(schedule_id: str):
|
||||
current_app._get_current_object(), # type: ignore[attr-defined]
|
||||
source_name=source_name, playbook_path=playbook_path,
|
||||
inventory_scope=inventory_scope, params=params,
|
||||
triggered_by=session["user_id"],
|
||||
triggered_by=session["user_id"], origin="schedule", origin_id=schedule_id,
|
||||
)
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
async with db.begin():
|
||||
|
||||
Reference in New Issue
Block a user