- /ansible/playbooks lists every playbook across sources: what it says it does, its category and destructive flag, how it last ran; the whole row opens the playbook. A source with none is one quiet line. - /ansible/playbooks/<source>/<path>: the verdict (what it does, flagged destructive, last ran), the run form in place (dry run beside the button, options folded), the schedules that use it, its recent runs, the playbook itself folded. ?from=<run> fills the form from that run: Run again on a run's page. - /ansible/browse, its view page and the /run-form fragment are gone, not redirected (rule 122); every link points at the new pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.3 KiB
HTML
104 lines
5.3 KiB
HTML
{# ansible/run_detail.html — one run (#5330): a verdict line (how it ended,
|
|
how long, when, who started it, what it did across its hosts), what it ran
|
|
with, each host's result in words, worst first, then its output. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header, status_badge %}
|
|
{% from "ansible/_tabs.html" import ansible_tabs %}
|
|
{% block title %}{{ row.title }} — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), (row.title, "")]) }}{% endblock %}
|
|
{% macro words(parts) -%}
|
|
{% for text, tone in parts %}{% if not loop.first %} · {% endif %}{% if tone %}<span class="tone-{{ tone }}">{{ text }}</span>{% else %}{{ text }}{% endif %}{% endfor %}
|
|
{%- endmacro %}
|
|
{% block content %}
|
|
{{ ansible_tabs("runs") }}
|
|
{% call page_header(row.title) %}
|
|
{% 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? Ansible stops where it is; tasks already applied stay applied." data-confirm-dismiss="Keep running">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-sm btn-danger">Cancel run</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if session.user_role in ("operator", "admin") %}
|
|
<a href="/ansible/playbooks/{{ run.source_name }}/{{ run.playbook_path }}?from={{ run.id }}#run-form" class="btn btn-sm btn-ghost">Run again</a>
|
|
{% endif %}
|
|
<a href="/ansible/runs/{{ run.id }}/log" class="btn btn-sm btn-ghost">Download log</a>
|
|
{% endcall %}
|
|
|
|
<p class="page-verdict">
|
|
{{- status_badge(run.status) }}
|
|
{%- if row.check %} <span class="tone-info" title="Ansible check mode: nothing was changed">dry run</span>{% endif %}
|
|
{%- if row.duration_s is not none %} {% if run.finished_at %}in{% else %}for{% endif %} {{ row.duration_s|duration }}{% if not run.finished_at %} so far{% endif %}{% endif %}
|
|
· started {{ run.started_at|ago }}
|
|
· by {% if origin.url %}<a href="{{ origin.url }}">{{ origin.text }}</a>{% else %}{{ origin.text }}{% endif %}
|
|
{%- if row.recap %} · {{ words(row.recap) }}{% endif -%}
|
|
</p>
|
|
|
|
<div class="card">
|
|
<dl class="kv">
|
|
<dt>Playbook</dt><dd><a href="/ansible/playbooks/{{ run.source_name }}/{{ run.playbook_path }}">{{ run.playbook_path }}</a> <span class="tone-dim">from {{ run.source_name }}</span></dd>
|
|
<dt>On</dt><dd>{{ row.scope_label }}</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 %}
|
|
<section class="card-flush" aria-labelledby="run-hosts">
|
|
<h2 id="run-hosts" class="table-caption">Hosts <span class="tone-muted">worst first</span></h2>
|
|
<table class="table">
|
|
<thead><tr><th>Host</th><th>Result</th></tr></thead>
|
|
<tbody>
|
|
{% for host, parts in hosts %}
|
|
<tr><td class="mono">{{ host }}</td><td>{{ words(parts) if parts else '—' }}</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{% endif %}
|
|
{% if run.results and run.results.failures %}
|
|
<section class="card" aria-labelledby="run-failures">
|
|
<h2 id="run-failures" class="section-title">What failed</h2>
|
|
<pre class="run-log run-failures">{{ run.results.failures | join("\n") }}</pre>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="card-flush" aria-labelledby="run-output">
|
|
<h2 id="run-output" class="table-caption">Output</h2>
|
|
{% if run.status.value in ("running", "queued") %}
|
|
<pre id="live-output" class="run-log">{{ run.output or "" }}</pre>
|
|
<p id="run-done-status" class="field-hint run-log-status">Streaming…</p>
|
|
<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 class="run-log">{{ run.output or "(no output)" }}</pre>
|
|
{% endif %}
|
|
</section>
|
|
{% endblock %}
|