Files
FabledSteward/steward/templates/hosts/detail.html
T
bvandeusen ad726e65f3
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m17s
CI / publish (push) Successful in 43s
feat(ansible): dropdown playbook selection + auto-populated variable fields
Stop making operators type playbook paths and guess extra-var names.

- Reusable infra: shared ansible/_playbook_vars.html (the discovered-variable
  fields) + ansible/_playbook_options.html; two HTMX endpoints —
  /ansible/playbook-options (a source's playbooks, optional ?selected for edit)
  and /ansible/playbook-vars (a playbook's vars:/vars_prompt: as fill-in
  fields). browse _run_form.html refactored to include the shared partial.
- Host "Run a playbook against this host": source dropdown → playbook dropdown
  → variable fields, all chained via HTMX. Handler reuses _parse_run_params so
  var__/secret__ fields flow through extra_vars_map + the unpersisted
  secret_vars channel.
- Schedules: playbook free-text+datalist → source-dependent dropdown; fixed the
  extra-vars edit pre-fill to read extra_vars_map (stale list key after the
  earlier JSON-extra-vars change).

Scribe #895.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:02:15 -04:00

172 lines
8.5 KiB
HTML

{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}{{ host.name }} — Hosts — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "")]) }}{% endblock %}
{% block content %}
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem;">
<div>
<h1 class="page-title" style="margin-bottom:0.2rem;">{{ host.name }}</h1>
<div style="color:var(--text-muted);font-size:0.9rem;font-family:ui-monospace,monospace;">
{{ host.address or "no address — resolves by name" }}
</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;">
<a href="/hosts/{{ host.id }}/edit" class="btn btn-ghost btn-sm">Edit</a>
{% if session.user_role == 'admin' %}
<form method="post" action="/hosts/{{ host.id }}/delete" style="margin:0;"
onsubmit="return confirm('Delete host {{ host.name }}? Monitors and history are removed.');">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
{% endif %}
</div>
</div>
{# ── Monitors ─────────────────────────────────────────────────────────────── #}
<div class="card">
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.75rem;">
<h3 class="section-title" style="margin-bottom:0;">Monitors</h3>
<a href="/hosts/{{ host.id }}/edit" class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Configure →</a>
</div>
<div style="display:flex;gap:2rem;flex-wrap:wrap;">
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">Ping</div>
{% if not host.ping_enabled %}
<div style="color:var(--text-dim);">off</div>
{% elif ping is none %}
<div style="color:var(--text-dim);">no data yet</div>
{% else %}
<div style="color:{{ 'var(--green)' if ping.status.value == 'up' else 'var(--red)' }};font-weight:600;">
{{ ping.status.value }}
</div>
{% if ping.response_time_ms is not none %}
<div style="font-size:0.8rem;color:var(--text-muted);">{{ '%.0f'|format(ping.response_time_ms) }} ms</div>
{% endif %}
{% endif %}
</div>
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">DNS</div>
{% if not host.dns_enabled %}
<div style="color:var(--text-dim);">off</div>
{% elif dns is none %}
<div style="color:var(--text-dim);">no data yet</div>
{% else %}
<div style="color:{{ 'var(--green)' if dns.status.value == 'resolved' else 'var(--red)' }};font-weight:600;">
{{ dns.status.value }}
</div>
{% if dns.resolved_ip %}
<div style="font-size:0.8rem;color:var(--text-muted);font-family:ui-monospace,monospace;">{{ dns.resolved_ip }}</div>
{% endif %}
{% endif %}
</div>
<div>
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">Uptime</div>
{% if uptime %}
<div style="font-size:0.85rem;">
24h <strong>{{ uptime['24h'] if uptime['24h'] is not none else '—' }}{{ '%' if uptime['24h'] is not none }}</strong>
· 7d <strong>{{ uptime['7d'] if uptime['7d'] is not none else '—' }}{{ '%' if uptime['7d'] is not none }}</strong>
· 30d <strong>{{ uptime['30d'] if uptime['30d'] is not none else '—' }}{{ '%' if uptime['30d'] is not none }}</strong>
</div>
{% else %}
<div style="color:var(--text-dim);"></div>
{% endif %}
</div>
</div>
</div>
{# ── Agent (host_agent plugin fragment, embedded across the plugin boundary) ── #}
<div hx-get="/plugins/host_agent/panel/{{ host.id }}" hx-trigger="load"
hx-swap="innerHTML">
<div class="card"><span style="color:var(--text-muted);font-size:0.85rem;">Loading agent…</span></div>
</div>
{# ── Ansible ──────────────────────────────────────────────────────────────── #}
<div class="card">
<h3 class="section-title">Ansible</h3>
{% if linked_target %}
<div style="display:flex;align-items:center;gap:1rem;padding:0.6rem 0.75rem;background:var(--bg-elevated);border-radius:4px;margin-bottom:1rem;flex-wrap:wrap;">
<div style="flex:1;min-width:12rem;">
<strong>{{ linked_target.name }}</strong>
<span style="color:var(--text-muted);font-size:0.85rem;margin-left:0.5rem;">{{ linked_target.address }}</span>
{% if linked_target.groups %}
<div style="margin-top:0.25rem;">
{% for grp in linked_target.groups %}
<span style="font-size:0.78rem;background:var(--bg);border-radius:3px;padding:0.1em 0.4em;margin-right:0.2rem;color:var(--text-muted);">{{ grp.name }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<a href="/ansible/inventory/targets/{{ linked_target.id }}" class="btn btn-ghost btn-sm">Edit target</a>
<form method="post" action="/hosts/{{ host.id }}/ansible-link" style="margin:0;">
<input type="hidden" name="action" value="unlink">
<button type="submit" class="btn btn-ghost btn-sm">Unlink</button>
</form>
</div>
{% if ansible_sources %}
<details>
<summary style="cursor:pointer;font-size:0.85rem;color:var(--text-muted);">Run a playbook against this host</summary>
<form method="post" action="/hosts/{{ host.id }}/run-playbook" style="margin-top:0.75rem;">
<div class="form-group">
<label>Source</label>
<select name="source_name" required
hx-get="/ansible/playbook-options" hx-trigger="change"
hx-target="#hp-playbook" hx-swap="innerHTML" hx-include="this">
<option value="">— choose source —</option>
{% for s in ansible_sources %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
</select>
</div>
<div class="form-group">
<label>Playbook</label>
<select name="playbook_path" id="hp-playbook" required
hx-get="/ansible/playbook-vars" hx-trigger="change"
hx-target="#hp-vars" hx-swap="innerHTML" hx-include="closest form">
<option value="">— choose a source first —</option>
</select>
</div>
<div id="hp-vars"></div>
<details style="margin:0.25rem 0 0.75rem;">
<summary style="cursor:pointer;font-size:0.82rem;color:var(--text-muted);">Advanced</summary>
<div class="form-group" style="margin-top:0.6rem;">
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(one key=value per line)</span></label>
<textarea name="extra_vars" rows="2" style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;"></textarea>
</div>
<div class="form-group">
<label>Tags <span style="color:var(--text-muted);font-weight:normal;">(--tags)</span></label>
<input type="text" name="tags" placeholder="deploy,config">
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;">
<input type="checkbox" name="check"> Dry-run (--check --diff)
</label>
</div>
</details>
<button type="submit" class="btn btn-sm">Run playbook</button>
</form>
</details>
{% endif %}
{% else %}
<p style="color:var(--text-muted);font-size:0.88rem;margin-bottom:1rem;">
No Ansible target linked. A target gives this host an SSH connection so Steward can
run playbooks and provision the agent.
</p>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:flex-end;">
{% if linkable_targets %}
<form method="post" action="/hosts/{{ host.id }}/ansible-link" style="display:flex;gap:0.5rem;align-items:center;">
<input type="hidden" name="action" value="link">
<select name="target_id">
{% for tgt in linkable_targets %}<option value="{{ tgt.id }}">{{ tgt.name }} ({{ tgt.address }})</option>{% endfor %}
</select>
<button type="submit" class="btn btn-sm">Link</button>
</form>
{% endif %}
<form method="post" action="/hosts/{{ host.id }}/ansible-link" style="margin:0;">
<input type="hidden" name="action" value="create">
<button type="submit" class="btn btn-sm btn-ghost">Create target from this host</button>
</form>
</div>
{% endif %}
</div>
{% endblock %}