feat(hosts): the Hosts list labels its figures and says how long; SLA tab gone; one place to add a host (#5279–#5283)
Hosts list: - CPU and Memory are their own labelled columns (#5279); a silent agent's last readings are greyed, and Status says "agent silent for 7m". - One uptime figure, 30 days, with 24h and 7d on hover. The SLA tab (/hosts/uptime) repeated it and is removed (#5280; one home per fact). - Status is a word with how long ("Down for 12m"); host_status now carries down_since. Edit and Delete live on the host's page only (#5281). - Figures are right-aligned with tabular digits (.table .num), not a monospace face. Agent fleet (#5282): its add-host form is gone, so hosts are added only on Hosts -> Add host. That form was also the only way to get a token for a manual curl install, so that moves to the host's Agent card as "Install manually" (POST /fleet/<host_id>/register), shown on the fleet page as before. "Delete" there reads "Unregister". Add host (#5283): laid out like the add-monitor form; a name clash comes back inline with what was typed instead of a bare text 400; the address is optional, as the host page already allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs %}
|
||||
{% block title %}{% if host %}Edit Host{% else %}New Host{% endif %} — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (("Edit " ~ host.name) if host else "New host", "")]) }}{% endblock %}
|
||||
{% from "_macros.html" import crumbs, page_header %}
|
||||
{# Add a host (host is none) or edit one; `values` holds what was typed when
|
||||
an error sends the form back. Laid out like the add-monitor form (#5283). #}
|
||||
{% block title %}{{ "Edit " ~ host.name if host else "Add host" }} — Steward{% endblock %}
|
||||
{% block breadcrumb %}
|
||||
{%- if host -%}
|
||||
{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Edit", "")]) }}
|
||||
{%- else -%}
|
||||
{{ crumbs([("Hosts", "/hosts/"), ("Add host", "")]) }}
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div style="max-width:560px;margin:2rem auto;">
|
||||
<h1 class="page-title">{% if host %}Edit Host{% else %}Add Host{% endif %}</h1>
|
||||
<div class="card">
|
||||
<form method="post" action="{% if host %}/hosts/{{ host.id }}{% else %}/hosts/{% endif %}">{{ csrf_field() }}
|
||||
<div class="form-group">
|
||||
<label>Display Name</label>
|
||||
<input type="text" name="name" value="{{ host.name if host else '' }}" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Address (hostname or IP)</label>
|
||||
<input type="text" name="address" value="{{ host.address if host else '' }}" required>
|
||||
</div>
|
||||
<div style="display:flex;gap:1rem;margin-top:1.5rem;">
|
||||
<button type="submit" class="btn">{% if host %}Save{% else %}Add Host{% endif %}</button>
|
||||
<a href="{% if host %}/hosts/{{ host.id }}{% else %}/hosts/{% endif %}" class="btn btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% set v = values or {} %}
|
||||
{{ page_header("Edit host" if host else "Add host") }}
|
||||
|
||||
<div class="card" style="max-width:760px;">
|
||||
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="{{ '/hosts/' ~ host.id if host else '/hosts/' }}" style="display:grid;gap:0.6rem;">{{ csrf_field() }}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.6rem;align-items:end;">
|
||||
<div>
|
||||
<label class="field-label" for="h-name">Name <span class="tone-crit" aria-hidden="true">*</span></label>
|
||||
<input type="text" id="h-name" name="name" value="{{ v.name if v.name is defined else (host.name if host else '') }}"
|
||||
placeholder="nas, web-1, …" autocomplete="off" required autofocus class="input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="h-address">Address</label>
|
||||
<input type="text" id="h-address" name="address" value="{{ v.address if v.address is defined else (host.address if host else '') }}"
|
||||
placeholder="hostname or IP; empty resolves by name" autocomplete="off" class="input">
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:var(--text-muted);font-size:0.82rem;margin-top:1rem;text-align:center;">
|
||||
{% if host %}Monitors, agent, Ansible target, and playbook runs live on the
|
||||
<a href="/hosts/{{ host.id }}">host page</a>.{% else %}
|
||||
After adding, attach ping / DNS / HTTP monitors from the host page.{% endif %}
|
||||
<p class="field-hint" style="margin:0;">
|
||||
{% if host %}Renaming carries the host's agent and container history, and the alert rules on it, to the new name.
|
||||
{% else %}Next, on the host's page: add monitors, install its agent, and link an Ansible target.{% endif %}
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button type="submit" class="btn btn-sm">{{ "Save" if host else "Add host" }}</button>
|
||||
<a href="{{ '/hosts/' ~ host.id if host else '/hosts/' }}" class="btn btn-sm btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import status_badge, status_dot %}
|
||||
{% from "_macros.html" import status_badge %}
|
||||
{# Hosts, problems first (host_svc.host_rank): a down monitor, then an agent
|
||||
gone quiet, then monitors not yet checked, then the healthy. #}
|
||||
{% macro pct(v) %}{% if v is none %}—{% else %}{{ '%.0f'|format(v) }}%{% endif %}{% endmacro %}
|
||||
gone quiet, then monitors not yet checked, then the healthy. Status says
|
||||
what is wrong and for how long; editing and deleting a host happen on its
|
||||
own page (#5281). Uptime is one figure, 30 days, with the shorter windows
|
||||
on hover: Monitors is where uptime is read in full (#5280). #}
|
||||
{% macro pct(v, kind, stale) -%}
|
||||
{%- if v is none %}<span class="tone-dim">—</span>
|
||||
{%- else %}<span class="tone-{{ 'dim' if stale else status_class(v, kind) }}">{{ '%.0f'|format(v) }}%</span>{% endif -%}
|
||||
{%- endmacro %}
|
||||
{% block title %}Hosts — Steward{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Hosts</h1>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<a class="btn btn-ghost" href="/hosts/uptime">SLA</a>
|
||||
{% if session.user_role == 'admin' %}
|
||||
<a class="btn btn-ghost" href="/plugins/host_agent/fleet/">Agent fleet</a>
|
||||
{% endif %}
|
||||
<a class="btn" href="/hosts/new">Add Host</a>
|
||||
<a class="btn" href="/hosts/new">Add host</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if hosts %}
|
||||
@@ -22,64 +27,49 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th style="text-align:center;" title="Worst current status across this host's monitors">Status</th>
|
||||
<th style="text-align:center;">Latency</th>
|
||||
<th style="text-align:center;" title="Number of monitors attached">Monitors</th>
|
||||
<th style="text-align:center;" title="Uptime last 24 hours">24h</th>
|
||||
<th style="text-align:center;" title="Uptime last 7 days">7d</th>
|
||||
<th style="text-align:center;" title="Uptime last 30 days">30d</th>
|
||||
<th style="text-align:center;" title="Agent CPU / memory (latest)">Agent</th>
|
||||
<th></th>
|
||||
<th title="The worst latest result across this host's monitors, and the agent if it has gone quiet">Status</th>
|
||||
<th class="num" title="Fastest latest response among this host's monitors">Latency</th>
|
||||
<th class="num" title="Monitors with a result">Monitors</th>
|
||||
<th class="num" title="Uptime over 30 days across this host's monitors; hover for 24h and 7d">30d uptime</th>
|
||||
<th class="num" title="Agent: CPU in use, latest reading">CPU</th>
|
||||
<th class="num" title="Agent: memory in use, latest reading">Memory</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for host in hosts %}
|
||||
{% set hs = host_status.get(host.id) %}
|
||||
{% set ut = uptime.get(host.id, {}) %}
|
||||
{% set am = agent.get(host.name) %}
|
||||
{% set silent = am and not am.fresh %}
|
||||
<tr>
|
||||
<td><a href="/hosts/{{ host.id }}" style="font-weight:500;">{{ host.name }}</a></td>
|
||||
<td style="color:var(--text-muted);">{{ host.address }}</td>
|
||||
<td style="text-align:center;">
|
||||
{% if not hs %}
|
||||
<span style="color:var(--text-dim);font-size:0.8rem;" title="No monitors">—</span>
|
||||
{% else %}{{ status_dot(hs.state) }}{% endif %}
|
||||
<td class="tone-muted">{{ host.address or "—" }}</td>
|
||||
<td>
|
||||
{% if hs %}
|
||||
{{ status_badge(hs.state) }}
|
||||
{%- if hs.state == 'down' and hs.down_since %} <span class="tone-dim">for {{ hs.down_since|ago|striptags|replace(" ago", "") }}</span>{% endif %}
|
||||
{% else %}
|
||||
<span class="tone-dim" title="No monitors with a result">—</span>
|
||||
{% endif %}
|
||||
{% if silent %}
|
||||
<div style="margin-top:0.15rem;">
|
||||
{{ status_badge('stale', label="agent silent", title="No agent report since " ~ (am.last_at.strftime('%Y-%m-%d %H:%M UTC') if am.last_at else 'it started')) }}
|
||||
{%- if am.last_at %} <span class="tone-dim">for {{ am.last_at|ago|striptags|replace(" ago", "") }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center;font-size:0.9rem;">
|
||||
<td class="num">
|
||||
{% if hs and hs.latency_ms is not none %}
|
||||
<span style="{{ threshold_style(hs.latency_ms, 'latency') or 'color:var(--green);' }}">{{ hs.latency_ms | round(1) }} ms</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-dim);">—</span>
|
||||
{% endif %}
|
||||
<span class="tone-{{ status_class(hs.latency_ms, 'latency') }}">{{ hs.latency_ms | round(1) }} ms</span>
|
||||
{% else %}<span class="tone-dim">—</span>{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center;font-size:0.85rem;color:var(--text-muted);">
|
||||
{{ hs.count if hs else 0 }}
|
||||
</td>
|
||||
{% for window in ["24h", "7d", "30d"] %}
|
||||
{% set pct = ut.get(window) %}
|
||||
<td style="text-align:center;font-size:0.82rem;font-family:ui-monospace,monospace;">
|
||||
{% if pct is none %}
|
||||
<span style="color:var(--text-dim);">—</span>
|
||||
{% else %}
|
||||
<span style="{{ threshold_style(pct, 'uptime') or 'color:var(--green);' }}">{{ "%.1f"|format(pct) }}%</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% set am = agent.get(host.name) %}
|
||||
<td style="text-align:center;font-size:0.8rem;font-family:ui-monospace,monospace;">
|
||||
{% if am %}
|
||||
<span style="color:var(--text-muted);" title="CPU / memory, latest">{{ pct(am.cpu_pct) }} / {{ pct(am.mem_used_pct) }}</span>
|
||||
{% if not am.fresh %}<div>{{ status_badge('stale', title="No agent report since " ~ (am.last_at.strftime('%Y-%m-%d %H:%M UTC') if am.last_at else 'it started')) }} <span class="tone-dim" style="font-family:inherit;">{{ am.last_at|ago }}</span></div>{% endif %}
|
||||
{% else %}
|
||||
<span style="color:var(--text-dim);" title="No agent on this host">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="td-actions">
|
||||
<a href="/hosts/{{ host.id }}/edit" class="btn btn-sm btn-ghost" style="margin-right:0.5rem;">Edit</a>
|
||||
<form method="post" action="/hosts/{{ host.id }}/delete" style="display:inline;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
data-confirm="Delete {{ host.name }}?">Delete</button>
|
||||
</form>
|
||||
<td class="num tone-muted">{{ hs.count if hs else "—" }}</td>
|
||||
<td class="num" title="24h {{ ('%.2f%%'|format(ut['24h'])) if ut.get('24h') is not none else '—' }} · 7d {{ ('%.2f%%'|format(ut['7d'])) if ut.get('7d') is not none else '—' }}">
|
||||
{% if ut.get('30d') is none %}<span class="tone-dim">—</span>
|
||||
{% else %}<span class="tone-{{ status_class(ut['30d'], 'uptime') }}">{{ "%.2f"|format(ut['30d']) }}%</span>{% endif %}
|
||||
</td>
|
||||
<td class="num"{% if silent %} title="Last reading before the agent went quiet"{% endif %}>{% if am %}{{ pct(am.cpu_pct, 'cpu', silent) }}{% else %}<span class="tone-dim" title="No agent on this host">—</span>{% endif %}</td>
|
||||
<td class="num"{% if silent %} title="Last reading before the agent went quiet"{% endif %}>{% if am %}{{ pct(am.mem_used_pct, 'mem', silent) }}{% else %}<span class="tone-dim" title="No agent on this host">—</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs %}
|
||||
{% block title %}Uptime / SLA — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), ("Uptime / SLA", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Uptime / SLA</h1>
|
||||
</div>
|
||||
|
||||
{# ── Summary pills ──────────────────────────────────────────────────────────── #}
|
||||
{% set total = hosts | length %}
|
||||
{% if total %}
|
||||
{% set healthy = namespace(count=0) %}
|
||||
{% for h in hosts %}
|
||||
{% set pct = uptime.get(h.id, {}).get("30d") %}
|
||||
{% if pct is not none and pct >= 99 %}{% set healthy.count = healthy.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:0.75rem;margin-bottom:1.5rem;">
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.3rem;">Hosts monitored</div>
|
||||
<span class="stat-val">{{ total }}</span>
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.3rem;">≥ 99% (30d)</div>
|
||||
<span class="stat-val" style="color:{% if healthy.count == total %}var(--green){% else %}var(--orange){% endif %};">
|
||||
{{ healthy.count }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── SLA table ──────────────────────────────────────────────────────────────── #}
|
||||
{% if hosts %}
|
||||
<div class="card-flush">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>Address</th>
|
||||
<th style="text-align:center;min-width:80px;" title="Uptime last 24 hours">24 h</th>
|
||||
<th style="text-align:center;min-width:80px;" title="Uptime last 7 days">7 d</th>
|
||||
<th style="text-align:center;min-width:80px;" title="Uptime last 30 days">30 d</th>
|
||||
<th style="min-width:160px;">30-day bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for host in hosts %}
|
||||
{% set ut = uptime.get(host.id, {}) %}
|
||||
<tr>
|
||||
<td style="font-weight:500;"><a href="/hosts/{{ host.id }}" style="color:inherit;">{{ host.name }}</a></td>
|
||||
<td style="color:var(--text-muted);font-size:0.85rem;">{{ host.address }}</td>
|
||||
|
||||
{% for window in ["24h", "7d", "30d"] %}
|
||||
{% set pct = ut.get(window) %}
|
||||
<td style="text-align:center;font-size:0.88rem;font-family:ui-monospace,monospace;">
|
||||
{% if pct is none %}
|
||||
<span style="color:var(--text-muted);" title="No data yet">—</span>
|
||||
{% elif pct >= 99.9 %}
|
||||
<span style="color:var(--green);">{{ "%.2f"|format(pct) }}%</span>
|
||||
{% elif pct >= 99 %}
|
||||
<span style="color:var(--yellow);">{{ "%.2f"|format(pct) }}%</span>
|
||||
{% elif pct >= 95 %}
|
||||
<span style="color:var(--orange);">{{ "%.2f"|format(pct) }}%</span>
|
||||
{% else %}
|
||||
<span style="color:var(--red);">{{ "%.2f"|format(pct) }}%</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
{# 30-day visual bar #}
|
||||
{% set pct30 = ut.get("30d") %}
|
||||
<td>
|
||||
{% if pct30 is none %}
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;">no data</span>
|
||||
{% else %}
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<div style="flex:1;height:8px;background:var(--bg-elevated);border-radius:4px;overflow:hidden;border:1px solid var(--border);">
|
||||
<div style="height:100%;width:{{ pct30 | round(1) }}%;background:{% if pct30 >= 99.9 %}var(--green){% elif pct30 >= 99 %}var(--yellow){% elif pct30 >= 95 %}var(--orange){% else %}var(--red){% endif %};border-radius:4px;transition:width 0.3s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="text-align:center;padding:3rem;">
|
||||
<p style="color:var(--text-muted);">No hosts under watch. <a href="/hosts/new">Summon one to the table.</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p style="color:var(--text-dim);font-size:0.78rem;margin-top:1rem;">
|
||||
Uptime aggregates every monitor attached to a host. Hosts with no monitors show —.
|
||||
</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user