Files
FabledSteward/steward/templates/alerts/rules_form.html
T
bvandeusen 389002fc6f
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m18s
CI / publish (push) Successful in 1m2s
feat(ui): breadcrumb navigation across nested pages
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>
2026-06-16 14:22:19 -04:00

192 lines
9.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}{% if rule %}Edit Rule{% else %}New Alert Rule{% endif %} — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("Edit rule" if rule else "New rule", "")]) }}{% endblock %}
{% block content %}
<div style="max-width:600px;margin:2rem auto;">
<h1 class="page-title">{% if rule %}Edit Rule{% else %}New Alert Rule{% endif %}</h1>
<div class="card">
<form method="post" action="{% if rule %}/alerts/rules/{{ rule.id }}{% else %}/alerts/rules{% endif %}">
<div class="form-group">
<label>Rule Name</label>
<input type="text" name="name" required autofocus
value="{{ rule.name if rule else '' }}"
placeholder="e.g. Home server down">
</div>
{# ── Source module ───────────────────────────────────────────────────── #}
<div class="form-group">
<label>Source</label>
<select name="source_module" required
hx-get="/alerts/api/rule_fields"
hx-trigger="change"
hx-target="#rule-fields"
hx-include="[name='source_module']">
{% for src in source_modules %}
<option value="{{ src }}" {% if src == initial_source %}selected{% endif %}>
{{ src }}
</option>
{% endfor %}
</select>
</div>
{# ── Dynamic resource + metric ────────────────────────────────────────── #}
<div id="rule-fields">
{# Inline the same partial logic so the page is fully usable without JS #}
<div class="form-group">
<label>Resource Name</label>
{% if resources %}
<select name="resource_name" required>
{% if not (rule and rule.resource_name) %}<option value="" disabled selected>— pick a resource —</option>{% endif %}
{% for r in resources %}
<option value="{{ r }}" {% if rule and r == rule.resource_name %}selected{% endif %}>{{ r }}</option>
{% endfor %}
</select>
{% else %}
<select name="resource_name" required disabled>
<option value="">No data yet — run monitors first</option>
</select>
<p style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;">
Resource names populate from live metric data. Start the scheduler and try again.
</p>
{% endif %}
</div>
<div class="form-group">
<label>Metric Name</label>
<select name="metric_name" required>
{% if not (rule and rule.metric_name) %}<option value="" disabled selected>— pick a metric —</option>{% endif %}
{% for m in metrics %}
<option value="{{ m }}" {% if rule and m == rule.metric_name %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select>
</div>
</div>
{# ── Condition ───────────────────────────────────────────────────────── #}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
<div class="form-group">
<label>Operator</label>
<select name="operator">
{% for val, label in operators %}
<option value="{{ val }}" {% if rule and rule.operator.value == val %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Threshold</label>
<input type="number" name="threshold" step="any" required
value="{{ rule.threshold if rule else '' }}"
placeholder="0.5">
</div>
</div>
<div class="form-group">
<label>Consecutive failures before FIRING</label>
<input type="number" name="consecutive_failures_required" min="1"
value="{{ rule.consecutive_failures_required if rule else 1 }}">
</div>
{# ── Metric reference ────────────────────────────────────────────────── #}
<details style="margin-bottom:1rem;">
<summary style="cursor:pointer;font-size:0.82rem;color:var(--text-muted);user-select:none;">
Metric reference
</summary>
<div style="margin-top:0.75rem;display:grid;gap:0.5rem;">
{% set descs = {
"ping": [("up", "1.0 = reachable, 0.0 = unreachable"),
("response_time_ms", "round-trip latency in milliseconds")],
"dns": [("resolved", "1.0 = resolved, 0.0 = failed"),
("ip_changed", "1.0 when resolved IP differs from expected")],
"traefik": [("request_rate", "requests/sec for this router"),
("error_rate", "fraction of 5xx responses (01)"),
("latency_p50_ms", "median response time ms"),
("latency_p95_ms", "p95 response time ms"),
("latency_p99_ms", "p99 response time ms"),
("response_bytes_rate", "bytes/sec of responses"),
("cert_expiry_days", "days until TLS cert expires")],
"unifi": [("is_up", "1.0 = WAN up, 0.0 = down"),
("latency_ms", "WAN latency in ms"),
("total_clients", "number of connected clients")],
"docker": [("cpu_pct", "container CPU usage %"),
("mem_pct", "container memory usage %")],
"http": [("is_up", "1.0 = check passed, 0.0 = failed"),
("response_ms", "HTTP response time in ms")],
} %}
{% for src, pairs in descs.items() %}
<div>
<div style="font-size:0.78rem;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.2rem;">{{ src }}</div>
{% for metric, desc in pairs %}
<div style="display:grid;grid-template-columns:160px 1fr;gap:0.5rem;font-size:0.78rem;padding:0.15rem 0;">
<code>{{ metric }}</code>
<span style="color:var(--text-muted);">{{ desc }}</span>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</details>
{# ── Ansible action (admin only) ─────────────────────────────────────── #}
{% if session.get("user_role") == "admin" %}
{% set act = rule.ansible_action if rule and rule.ansible_action else None %}
<div style="margin:1.25rem 0;padding:1rem;border:1px solid var(--border-mid);border-radius:6px;background:var(--bg-elevated);">
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:600;">
<input type="checkbox" name="ansible_enabled" {% if act %}checked{% endif %}>
On firing → run an Ansible playbook
</label>
<p style="font-size:0.78rem;color:var(--text-muted);margin:0.4rem 0 0.75rem;">
Admin only. Runs once when the alert transitions into FIRING.
<code>steward_alert_*</code> context vars (resource, metric, value…) are injected automatically.
</p>
<div class="form-group">
<label>Source</label>
<select name="ansible_source">
<option value="">— select source —</option>
{% for s in ansible_sources %}
<option value="{{ s }}" {% if act and act.source == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Playbook <span style="color:var(--text-muted);font-weight:normal;">(path within source)</span></label>
<input type="text" name="ansible_playbook" placeholder="playbooks/shutdown.yml"
value="{{ act.playbook if act else '' }}">
</div>
<div class="form-group">
<label>Inventory <span style="color:var(--text-muted);font-weight:normal;">(path within source)</span></label>
<input type="text" name="ansible_inventory" placeholder="inventory/hosts"
value="{{ act.inventory if act else '' }}">
</div>
<div class="form-group">
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(optional, one key=value per line)</span></label>
<textarea name="ansible_extra_vars" rows="2"
style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;">{{ act.extra_vars | join("\n") if act and act.extra_vars else "" }}</textarea>
</div>
<div class="form-group">
<label>Limit to host(s) <span style="color:var(--text-muted);font-weight:normal;">(optional)</span></label>
<input type="text" name="ansible_limit" value="{{ act.limit if act and act.limit else '' }}">
</div>
<div class="form-group">
<label>Tags <span style="color:var(--text-muted);font-weight:normal;">(optional)</span></label>
<input type="text" name="ansible_tags" value="{{ act.tags if act and act.tags else '' }}">
</div>
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;">
<input type="checkbox" name="ansible_check" {% if act and act.check %}checked{% endif %}>
Dry-run (--check --diff)
</label>
</div>
{% endif %}
<div style="display:flex;gap:1rem;margin-top:1.5rem;">
<button type="submit" class="btn">{% if rule %}Save Changes{% else %}Create Rule{% endif %}</button>
<a href="/alerts/" class="btn btn-ghost">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}