#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>
104 lines
5.7 KiB
HTML
104 lines
5.7 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header, status_badge %}
|
|
{% block title %}{{ row.title }} — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), (row.title, "")]) }}{% endblock %}
|
|
{% block content %}
|
|
{% call page_header(row.title, "From the " ~ run.source_name ~ " playbook source") %}
|
|
{% if run.status.value in ("running", "queued") and session.user_role in ("operator", "admin") %}
|
|
<form method="post" action="/ansible/runs/{{ run.id }}/cancel" data-confirm="Cancel this run?">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-sm btn-danger">Cancel run</button>
|
|
</form>
|
|
{% endif %}
|
|
<a href="/ansible/runs/{{ run.id }}/log" class="btn btn-sm btn-ghost">Download log</a>
|
|
{% endcall %}
|
|
|
|
<div class="card">
|
|
<dl class="kv">
|
|
<dt>Status</dt><dd>{{ status_badge(run.status) }}{% if row.check %} <span class="badge badge-dim" title="Ansible check mode: nothing was changed">dry run</span>{% endif %}</dd>
|
|
<dt>Started</dt><dd title="{{ run.started_at.strftime('%Y-%m-%d %H:%M:%S UTC') }}">{{ run.started_at|ago }}</dd>
|
|
<dt>Duration</dt><dd>{{ row.duration_s|duration }}{% if not run.finished_at %} so far{% endif %}</dd>
|
|
<dt>Playbook</dt><dd>{{ run.playbook_path }}</dd>
|
|
<dt>Scope</dt><dd title="{{ run.inventory_scope }}">{{ row.scope_label }}</dd>
|
|
<dt>Source</dt><dd>{{ run.source_name }}</dd>
|
|
<dt>Started by</dt><dd>{% if origin.url %}<a href="{{ origin.url }}">{{ origin.text }}</a>{% else %}{{ origin.text }}{% endif %}</dd>
|
|
{% if run.params %}
|
|
{% if run.params.limit %}<dt>Limit</dt><dd class="mono">{{ run.params.limit }}</dd>{% endif %}
|
|
{% if run.params.tags %}<dt>Tags</dt><dd class="mono">{{ run.params.tags }}</dd>{% endif %}
|
|
{% if run.params.extra_vars %}<dt>Extra vars</dt><dd class="mono">{{ run.params.extra_vars | join(", ") }}</dd>{% endif %}
|
|
{% endif %}
|
|
</dl>
|
|
</div>
|
|
|
|
{% if hosts %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.6rem;">Host summary</div>
|
|
<table class="table">
|
|
<thead><tr>
|
|
<th>Host</th>
|
|
<th style="text-align:center;">ok</th><th style="text-align:center;">changed</th>
|
|
<th style="text-align:center;">unreachable</th><th style="text-align:center;">failed</th>
|
|
<th style="text-align:center;">skipped</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for host, c in hosts %}
|
|
<tr>
|
|
<td style="font-family:ui-monospace,monospace;">{{ host }}</td>
|
|
<td style="text-align:center;">{{ c.ok }}</td>
|
|
<td style="text-align:center;color:{% if c.changed %}var(--yellow){% else %}var(--text-dim){% endif %};">{{ c.changed }}</td>
|
|
<td style="text-align:center;color:{% if c.unreachable %}var(--red){% else %}var(--text-dim){% endif %};">{{ c.unreachable }}</td>
|
|
<td style="text-align:center;color:{% if c.failed %}var(--red){% else %}var(--text-dim){% endif %};">{{ c.failed }}</td>
|
|
<td style="text-align:center;color:var(--text-dim);">{{ c.skipped }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if run.results.failures %}
|
|
<div class="section-title" style="margin:1rem 0 0.4rem;">Failures</div>
|
|
<pre style="margin:0;padding:0.75rem;background:var(--bg);border-radius:4px;font-size:0.75rem;color:var(--red);max-height:240px;overflow:auto;white-space:pre-wrap;">{{ run.results.failures | join("\n") }}</pre>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card" style="padding:0;">
|
|
<div style="padding:0.75rem 1rem;background:var(--bg-elevated);border-bottom:1px solid var(--border);">
|
|
<span style="color:var(--text-muted);font-size:0.85rem;">Output</span>
|
|
</div>
|
|
{% if run.status.value in ("running", "queued") %}
|
|
<pre id="live-output"
|
|
style="margin:0;padding:1rem;background:var(--bg);font-size:0.78rem;color:var(--green);max-height:600px;overflow-y:auto;white-space:pre-wrap;">{{ run.output or "" }}</pre>
|
|
<div id="run-done-status" style="padding:0.5rem 1rem;font-size:0.85rem;color:var(--text-muted);">
|
|
Streaming…
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var es = new EventSource('/ansible/runs/{{ run.id }}/stream');
|
|
var pre = document.getElementById('live-output');
|
|
var status = document.getElementById('run-done-status');
|
|
// Follow new lines only while the reader is at the bottom; scrolled
|
|
// up to read something, they keep their place.
|
|
es.addEventListener('output', function(e) {
|
|
var atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 24;
|
|
pre.textContent += e.data + '\n';
|
|
if (atBottom) pre.scrollTop = pre.scrollHeight;
|
|
});
|
|
// Finished: load the page again, so the status, duration and host
|
|
// summary are the run's final ones rather than "Running". The
|
|
// executor saves the final status before it says done; should that
|
|
// save have failed, ?finished stops a second reload going round.
|
|
es.addEventListener('done', function(e) {
|
|
es.close();
|
|
if (window.location.search.indexOf('finished=1') === -1) {
|
|
status.textContent = 'Finished. Loading the summary…';
|
|
window.location.replace(window.location.pathname + '?finished=1');
|
|
} else {
|
|
status.textContent = 'Finished (' + e.data + '), but the summary could not be saved.';
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% else %}
|
|
<pre style="margin:0;padding:1rem;background:var(--bg);font-size:0.78rem;color:var(--text-muted);max-height:600px;overflow-y:auto;white-space:pre-wrap;">{{ run.output or "(no output)" }}</pre>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|