389002fc6f
Adds a breadcrumb trail to nested views for orientation. Mechanism: a
{% block breadcrumb %} slot in base.html (+ styling) and a shared crumbs()
macro in templates/_macros.html; each nested page fills the block with its
trail (root→current, last item is the current page). Pages without the block
render no bar, so top-level nav roots stay clean.
Applied to: Ansible (browse, schedules, playbook editor, run detail) +
inventory (targets/groups + detail), host_agent (fleet, host detail,
settings), hosts form, settings tabs (ansible/auth/notifications/plugins/
reports + plugin detail), dashboard (list, edit), and alerts (rule form,
maintenance + new). Dynamic labels (host/target/run/dashboard names) come
from the page context — no route changes. All 60 templates Jinja-compile.
Task #873.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
148 lines
7.7 KiB
HTML
148 lines
7.7 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs %}
|
|
{% block title %}Ansible Schedules — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Schedules", "")]) }}{% endblock %}
|
|
|
|
{% macro fmt_interval(secs) %}
|
|
{%- set m = {300: "every 5 min", 3600: "hourly", 21600: "every 6h", 43200: "every 12h", 86400: "daily", 604800: "weekly"} -%}
|
|
{{ m.get(secs, secs ~ "s") }}
|
|
{% endmacro %}
|
|
|
|
{% block content %}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;gap:1rem;flex-wrap:wrap;">
|
|
<h1 class="page-title" style="margin-bottom:0;">Ansible Schedules</h1>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
<a href="/ansible/inventory/targets" class="btn btn-ghost">Inventory</a>
|
|
<a href="/ansible/browse" class="btn btn-ghost">Browse</a>
|
|
<a href="/ansible/" class="btn btn-ghost">← Runs</a>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Existing schedules ───────────────────────────────────────────────────── #}
|
|
{% if not rows %}
|
|
<div class="card empty">No schedules yet. Create one below to run a playbook on a recurring interval.</div>
|
|
{% else %}
|
|
<div class="card-flush">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th><th>Playbook</th><th>Target</th><th>Interval</th>
|
|
<th>Last run</th><th>Next run</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in rows %}
|
|
{% set s = row.s %}
|
|
<tr style="{% if not s.enabled %}opacity:0.55;{% endif %}">
|
|
<td style="font-weight:500;">{{ s.name }}</td>
|
|
<td style="font-size:0.85rem;"><span style="color:var(--text-dim);">{{ s.source_name }}</span> / {{ s.playbook_path }}</td>
|
|
<td style="font-size:0.82rem;color:var(--text-muted);font-family:ui-monospace,monospace;">{{ s.inventory_scope }}</td>
|
|
<td style="font-size:0.85rem;">{{ fmt_interval(s.interval_seconds) }}</td>
|
|
<td style="font-size:0.82rem;">
|
|
{% if s.last_error %}
|
|
<span style="color:var(--red);" title="{{ s.last_error }}">launch error</span>
|
|
{% elif row.last_run %}
|
|
{% set sc = {"running":"var(--yellow)","success":"var(--green)","failed":"var(--red)","interrupted":"var(--orange)"}.get(row.last_run.status.value, "var(--text-muted)") %}
|
|
<a href="/ansible/runs/{{ row.last_run.id }}" style="color:{{ sc }};font-weight:600;">{{ row.last_run.status.value }}</a>
|
|
<span style="color:var(--text-dim);">· {{ s.last_run_at.strftime("%m-%d %H:%M") }}</span>
|
|
{% else %}
|
|
<span style="color:var(--text-dim);">never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td style="font-size:0.82rem;color:var(--text-muted);">
|
|
{% if not s.enabled %}<span style="color:var(--text-dim);">paused</span>
|
|
{% elif row.next_run %}{{ row.next_run.strftime("%m-%d %H:%M") }}
|
|
{% else %}due now{% endif %}
|
|
</td>
|
|
<td class="td-actions" style="white-space:nowrap;">
|
|
<form method="post" action="/ansible/schedules/{{ s.id }}/run-now" style="display:inline;">
|
|
<button class="btn btn-sm btn-ghost" type="submit">Run now</button>
|
|
</form>
|
|
<form method="post" action="/ansible/schedules/{{ s.id }}/toggle" style="display:inline;">
|
|
<button class="btn btn-sm btn-ghost" type="submit">{{ "Pause" if s.enabled else "Resume" }}</button>
|
|
</form>
|
|
<a href="?edit={{ s.id }}#form" class="btn btn-sm btn-ghost">Edit</a>
|
|
<form method="post" action="/ansible/schedules/{{ s.id }}/delete" style="display:inline;"
|
|
onsubmit="return confirm('Delete schedule {{ s.name }}?');">
|
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Create / edit form ───────────────────────────────────────────────────── #}
|
|
{% set p = (editing.params or {}) if editing else {} %}
|
|
<div class="card" id="form">
|
|
<h2 class="section-title" style="margin-bottom:1rem;">{{ "Edit schedule" if editing else "New schedule" }}</h2>
|
|
<form method="post" action="{{ '/ansible/schedules/' ~ editing.id if editing else '/ansible/schedules' }}">
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;">
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label>Name</label>
|
|
<input type="text" name="name" required value="{{ editing.name if editing else '' }}" placeholder="Nightly docker prune">
|
|
</div>
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label>Source</label>
|
|
<select name="source_name" required>
|
|
<option value="">— choose source —</option>
|
|
{% for sd in source_data %}
|
|
<option value="{{ sd.name }}" {% if editing and editing.source_name == sd.name %}selected{% endif %}>{{ sd.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label>Playbook path</label>
|
|
<input type="text" name="playbook_path" required list="playbooks"
|
|
value="{{ editing.playbook_path if editing else '' }}" placeholder="maintenance/docker_prune.yml">
|
|
<datalist id="playbooks">
|
|
{% for pb in all_playbooks %}<option value="{{ pb }}"></option>{% endfor %}
|
|
</datalist>
|
|
</div>
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<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" style="margin-bottom:0;">
|
|
<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" style="margin-bottom:0;">
|
|
<label>Limit (optional)</label>
|
|
<input type="text" name="limit" value="{{ p.get('limit', '') }}" placeholder="host or group pattern">
|
|
</div>
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label>Tags (optional)</label>
|
|
<input type="text" name="tags" value="{{ p.get('tags', '') }}" placeholder="tag1,tag2">
|
|
</div>
|
|
</div>
|
|
<div class="form-group" style="margin-top:1rem;">
|
|
<label>Extra vars (one key=value per line)</label>
|
|
<textarea name="extra_vars" rows="3" placeholder="prune_volumes=false">{{ p.get('extra_vars', []) | join('\n') }}</textarea>
|
|
</div>
|
|
<div class="form-group" style="display:flex;align-items:center;gap:0.5rem;">
|
|
<input type="checkbox" name="check" id="check" style="width:auto;" {% if p.get('check') %}checked{% endif %}>
|
|
<label for="check" style="margin-bottom:0;">Dry run (--check --diff)</label>
|
|
</div>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
<button type="submit" class="btn">{{ "Save schedule" if editing else "Create schedule" }}</button>
|
|
{% if editing %}<a href="/ansible/schedules" class="btn btn-ghost">Cancel</a>{% endif %}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|