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>
142 lines
7.3 KiB
HTML
142 lines
7.3 KiB
HTML
{# ansible/schedule.html — one schedule's page (#5332): when it runs next and
|
|
how it last went, its form (operators), and at the foot Delete behind the
|
|
confirm dialog. /ansible/schedules/new is the same page, empty. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header, status_badge %}
|
|
{% from "ansible/_tabs.html" import ansible_tabs %}
|
|
{% from "ansible/_macros.html" import interval_words %}
|
|
{% block title %}{{ editing.name if editing else "New schedule" }} — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Schedules", "/ansible/schedules"), (editing.name if editing else "New schedule", "")]) }}{% endblock %}
|
|
{% block section_tabs %}{{ ansible_tabs("schedules") }}{% endblock %}
|
|
{% block content %}
|
|
{% set can_edit = session.user_role in ('operator', 'admin') %}
|
|
{% call page_header(editing.name if editing else "New schedule") %}
|
|
{% if editing and can_edit %}
|
|
<form method="post" action="/ansible/schedules/{{ editing.id }}/run-now">{{ csrf_field() }}
|
|
<button class="btn btn-sm btn-ghost" type="submit">Run now</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endcall %}
|
|
|
|
{% if editing %}
|
|
<p class="page-verdict">
|
|
<a href="/ansible/playbooks/{{ editing.source_name }}/{{ editing.playbook_path }}" class="mono">{{ editing.playbook_path }}</a>
|
|
on {{ scope_label }} · {{ interval_words(editing.interval_seconds) }}
|
|
{%- if (editing.params or {}).get('check') %} · <span class="tone-info">dry run</span>{% endif %}
|
|
· {% if not editing.enabled %}<span class="tone-dim">paused</span>{% elif next and next.at %}next {{ next.at|ago }}{% else %}due now{% endif %}
|
|
{%- if last_run %} · last: <a href="/ansible/runs/{{ last_run.id }}">{{ status_badge(last_run.status) }}</a> {{ last_run.started_at|ago }}{% elif not editing.last_error %} · never run{% endif %}
|
|
{%- if editing.last_error %} · <span class="{{ 'tone-muted' if last_run else 'tone-crit' }}">{{ editing.last_error }}</span>{% endif -%}
|
|
</p>
|
|
{% endif %}
|
|
|
|
{# ── Create / edit form (operators and admins) ───────────────────────────── #}
|
|
{% if can_edit %}
|
|
{% set p = (editing.params or {}) if editing else {} %}
|
|
{% set saved_vars = p.get('extra_vars_map') or {} %}
|
|
{% set declared_names = (edit_variables or []) | map(attribute='name') | list %}
|
|
<div class="card" id="form">
|
|
<form method="post" action="{{ '/ansible/schedules/' ~ editing.id if editing else '/ansible/schedules' }}">{{ csrf_field() }}
|
|
<div class="form-pair">
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<input type="text" name="name" required value="{{ editing.name if editing else '' }}" placeholder="Nightly docker prune">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Source</label>
|
|
<select name="source_name" required
|
|
hx-get="/ansible/playbook-options" hx-trigger="change"
|
|
hx-target="#sch-playbook" hx-swap="innerHTML" hx-include="this"
|
|
hx-on::after-settle="htmx.trigger('#sch-playbook','change')">
|
|
{% for sd in source_data %}
|
|
<option value="{{ sd.name }}" {% if sd.name == sel_source %}selected{% endif %}>{{ sd.name }}</option>
|
|
{% endfor %}
|
|
{% if not source_data %}<option value="" disabled>No Ansible sources — add one in Settings</option>{% endif %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Playbook</label>
|
|
<select name="playbook_path" id="sch-playbook" required
|
|
hx-get="/ansible/playbook-vars?schedule=1"
|
|
hx-trigger="change"
|
|
hx-target="#sch-playbook-vars" hx-swap="innerHTML" hx-include="closest form">
|
|
{% for pb in sel_playbooks %}
|
|
<option value="{{ pb }}" {% if pb == sel_playbook %}selected{% endif %}>{{ pb }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Target</label>
|
|
<select name="inventory_scope">
|
|
<option value="steward:all" {% if editing and editing.inventory_scope == 'steward:all' %}selected{% endif %}>All targets</option>
|
|
{% for g in groups %}
|
|
<option value="steward:group:{{ g.id }}" {% if editing and editing.inventory_scope == 'steward:group:' ~ g.id %}selected{% endif %}>Group: {{ g.name }}</option>
|
|
{% endfor %}
|
|
{% for t in targets %}
|
|
<option value="steward:target:{{ t.id }}" {% if editing and editing.inventory_scope == 'steward:target:' ~ t.id %}selected{% endif %}>Target: {{ t.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Interval</label>
|
|
<select name="interval_seconds" required>
|
|
{% for secs, label in interval_presets %}
|
|
<option value="{{ secs }}" {% if editing and editing.interval_seconds == secs %}selected{% elif not editing and secs == 86400 %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Limit (optional)</label>
|
|
<input type="text" name="limit" value="{{ p.get('limit', '') }}" placeholder="host or group pattern">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Tags (optional)</label>
|
|
<input type="text" name="tags" value="{{ p.get('tags', '') }}" placeholder="tag1,tag2">
|
|
</div>
|
|
</div>
|
|
|
|
{# Declared playbook variables (vars:/vars_prompt:) — loaded via HTMX on
|
|
playbook change; pre-rendered here in edit mode so saved values show. #}
|
|
<div id="sch-playbook-vars">
|
|
{% if sel_playbook %}
|
|
{% set variables = edit_variables %}
|
|
{% set description = edit_description %}
|
|
{% set category = edit_category %}
|
|
{% set confirm = edit_confirm %}
|
|
{% set values = saved_vars %}
|
|
{% set schedule = true %}
|
|
{% include "ansible/_playbook_vars.html" %}
|
|
{% endif %}
|
|
</div>
|
|
|
|
<details class="run-more">
|
|
<summary>Extra vars</summary>
|
|
<div class="form-group">
|
|
<label for="sch-extra">Extra vars <span class="field-hint">one key=value per line, for vars not listed above</span></label>
|
|
<textarea id="sch-extra" name="extra_vars" rows="3" class="mono" placeholder="prune_volumes=false">{% for k, v in saved_vars.items() if k not in declared_names %}{{ k }}={{ v }}
|
|
{% endfor %}</textarea>
|
|
</div>
|
|
</details>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn">{{ "Save schedule" if editing else "Create schedule" }}</button>
|
|
<a href="/ansible/schedules" class="btn btn-ghost">Cancel</a>
|
|
<label class="field-check">
|
|
<input type="checkbox" name="check"{% if p.get('check') %} checked{% endif %}>
|
|
Dry run each time: show what would change, change nothing
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if editing and can_edit %}
|
|
<section class="danger-zone" aria-labelledby="sched-delete">
|
|
<h2 id="sched-delete" class="section-title">Delete this schedule</h2>
|
|
<p class="field-hint">Its past runs stay in the Runs list. Pausing it keeps it.</p>
|
|
<form method="post" action="/ansible/schedules/{{ editing.id }}/delete"
|
|
data-confirm="Delete schedule {{ editing.name }}? It stops running; its past runs stay in the list. Pausing it keeps it.">{{ csrf_field() }}
|
|
<button class="btn btn-danger" type="submit">Delete schedule</button>
|
|
</form>
|
|
</section>
|
|
{% endif %}
|
|
{% endblock %}
|