Files
FabledSteward/steward/templates/ansible/schedule.html
T
bvandeusenandClaude Opus 5.5 955450d33f
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m36s
fix(ui): section tabs above the breadcrumb, so the Ansible tab strip holds still (#5214)
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>
2026-10-08 06:28:02 -04:00

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 %}