feat(hosts): a host's page stays short — Docker folds past the busiest, monitors add on their own form, agent actions under Manage (#5276, #5277, #5278)
Docker panel (#5276): problems first, then the 8 busiest running containers by CPU; the other running and the stopped fold behind one "+N more running · M stopped" line. Containers that are not swarm tasks read the way the Docker page reads them: CI jobs as "CI: workflow · job", buildx builders as "BuildKit builder · name". Monitors card (#5277): the edit/delete icon buttons and the inline add form are gone. Each row links to its monitor's page; "Add monitor" opens the add form filed under this host, starting from its address, with the host in its breadcrumbs and Cancel back to it. Agent card (#5278): Full metrics stays in view; Update, Rotate token, Unregister agent (was "Remove") and Re-provision sit under "Manage agent". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,11 +85,9 @@ async def host_detail(host_id: str):
|
||||
select(AnsibleTarget).where(AnsibleTarget.host_id.is_(None))
|
||||
.order_by(AnsibleTarget.name))).scalars().all()
|
||||
|
||||
from steward.models.monitors import MonitorType
|
||||
return await render_template(
|
||||
"hosts/detail.html",
|
||||
host=host, monitors=monitors, uptime=uptime, alerts=alerts,
|
||||
monitor_types=list(MonitorType),
|
||||
linked_target=linked_target, linkable_targets=linkable_targets,
|
||||
ansible_sources=_ansible_source_names(),
|
||||
)
|
||||
|
||||
@@ -88,11 +88,21 @@ async def add_monitor():
|
||||
|
||||
|
||||
async def _new_form(form, mtype: str, error: str | None = None) -> str:
|
||||
"""The add form, holding what was typed when `error` sends it back."""
|
||||
"""The add form, holding what was typed when `error` sends it back. Opened
|
||||
from a host's page (host_id), it files the monitor under that host and
|
||||
starts from the host's address."""
|
||||
values = {k: form.get(k, "") for k in ("name", "target", "host_id")}
|
||||
values["check_interval_seconds"] = form.get("check_interval_seconds", 0)
|
||||
host = None
|
||||
if values["host_id"]:
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
host = await db.get(Host, values["host_id"])
|
||||
if host is None:
|
||||
values["host_id"] = ""
|
||||
elif not values["target"]:
|
||||
values["target"] = host.address or host.name
|
||||
return await render_template(
|
||||
"monitors/form.html", monitor=None, values=values,
|
||||
"monitors/form.html", monitor=None, values=values, host=host,
|
||||
mtype=mtype if mtype in MONITOR_TYPE_VALUES else MonitorType.icmp.value,
|
||||
config=_form_opts(form, mtype) if error and mtype in MONITOR_TYPE_VALUES else {},
|
||||
types=list(MonitorType), error=error,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs, status_dot, status_badge, icon_button %}
|
||||
{% from "monitors/_fields.html" import type_fields, toggle_script %}
|
||||
{% from "_macros.html" import crumbs, status_dot, status_badge %}
|
||||
{% block title %}{{ host.name }} — Hosts — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -58,6 +57,9 @@
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.5rem;">
|
||||
<h3 class="section-title" style="margin-bottom:0;">Monitors</h3>
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
<a href="/monitors/new?host_id={{ host.id }}" class="btn btn-ghost btn-sm" style="margin-left:auto;margin-right:0.75rem;">Add monitor</a>
|
||||
{% endif %}
|
||||
{% if uptime %}
|
||||
<span style="font-size:0.8rem;color:var(--text-muted);">
|
||||
uptime{% for w in ("24h", "7d", "30d") %}{{ " ·" if not loop.first }} {{ w }}
|
||||
@@ -94,54 +96,14 @@
|
||||
<span class="tone-{{ status_class(latest.response_ms, 'latency') }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
|
||||
{% else %}{{ status_badge('up') }}{% endif %}
|
||||
</div>
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
<div class="row-actions">
|
||||
{{ icon_button("✎", "Edit " ~ m.name, href="/monitors/" ~ m.id ~ "/edit") }}
|
||||
<form method="post" action="/monitors/{{ m.id }}/delete">{{ csrf_field() }}
|
||||
{{ icon_button("×", "Delete " ~ m.name, type="submit", confirm="Delete monitor " ~ m.name ~ "?") }}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:var(--text-muted);font-size:0.88rem;margin:0.25rem 0 0.75rem;">
|
||||
No monitors yet for this host. Add one below.
|
||||
No monitors yet for this host.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
<details style="margin-top:0.75rem;">
|
||||
<summary style="cursor:pointer;font-size:0.85rem;color:var(--text-muted);">+ Add monitor for this host</summary>
|
||||
<form method="post" action="/monitors/add" style="display:grid;gap:0.6rem;margin-top:0.75rem;">{{ csrf_field() }}
|
||||
<input type="hidden" name="host_id" value="{{ host.id }}">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:0.6rem;align-items:end;">
|
||||
<div>
|
||||
<label class="field-label">Type</label>
|
||||
<select name="type" id="mtype" onchange="mtoggle(this.value)" class="input">
|
||||
{% for t in monitor_types %}
|
||||
<option value="{{ t.value }}" {% if t.value == 'icmp' %}selected{% endif %}>{{ t.value | upper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Name</label>
|
||||
<input type="text" name="name" placeholder="optional" autocomplete="off" class="input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Target</label>
|
||||
<input type="text" name="target" value="{{ host.address }}" autocomplete="off" required class="input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Interval (s, 0=global)</label>
|
||||
<input type="number" name="check_interval_seconds" value="0" min="0" class="input">
|
||||
</div>
|
||||
{{ type_fields() }}
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-sm">Add Monitor</button></div>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>{# Monitors card #}
|
||||
|
||||
{# ── Agent (host_agent management panel) ──────────────────────────────────── #}
|
||||
@@ -249,6 +211,4 @@
|
||||
<div hx-get="/plugins/snmp/host/{{ host.id }}" hx-trigger="load" hx-swap="innerHTML"></div>
|
||||
{% endif %}
|
||||
|
||||
{{ toggle_script() }}
|
||||
<script>var _mt=document.getElementById('mtype'); if(_mt) mtoggle(_mt.value);</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
{% block breadcrumb %}
|
||||
{%- if monitor -%}
|
||||
{{ crumbs([("Monitors", "/monitors/"), (monitor.name, "/monitors/" ~ monitor.id), ("Edit", "")]) }}
|
||||
{%- elif host -%}
|
||||
{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Add monitor", "")]) }}
|
||||
{%- else -%}
|
||||
{{ crumbs([("Monitors", "/monitors/"), ("Add monitor", "")]) }}
|
||||
{%- endif -%}
|
||||
@@ -47,10 +49,12 @@
|
||||
</div>
|
||||
{% if monitor and monitor.host_id %}
|
||||
<p class="field-hint">Linked to <a href="/hosts/{{ monitor.host_id }}">its host</a>.</p>
|
||||
{% elif host %}
|
||||
<p class="field-hint">Filed under <a href="/hosts/{{ host.id }}">{{ host.name }}</a>; it shows on that host's page.</p>
|
||||
{% endif %}
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button type="submit" class="btn btn-sm">{{ "Save" if monitor else "Add monitor" }}</button>
|
||||
<a href="{{ '/monitors/' ~ monitor.id if monitor else '/monitors/' }}" class="btn btn-sm btn-ghost">Cancel</a>
|
||||
<a href="{{ '/monitors/' ~ monitor.id if monitor else ('/hosts/' ~ host.id if host else '/monitors/') }}" class="btn btn-sm btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user