The tabs were drawn in each page's content block, which base.html renders after the breadcrumb, so on every nested Ansible page the strip moved down a line. base.html now has a section_tabs block above the breadcrumb, and every Ansible page fills it. A guard in test_breadcrumbs.py fails any page that calls a tabs macro outside that block. 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 section_tabs %}{{ ansible_tabs("runs") }}{% endblock %}
|
|
{% block content %}
|
|
{% 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 %}
|