#5290 "Host page: the health tiles span the card's width; even spacing above Ansible": the four tiles take equal quarters (halves on a phone) and their sparklines stretch across them; the Monitors + Agent row drops the Agent card's own margin, so the gap above Ansible is 1rem like every other. #5291 "Add host opens as a dialog over the Hosts list": a native <dialog> on /hosts/, opened by Add host or /hosts/?add=1. A name clash re-renders the list with it open, the error and what was typed. GET /hosts/new is gone; hosts/form.html is Edit only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
6.6 KiB
HTML
114 lines
6.6 KiB
HTML
{% extends "base.html" %}
|
|
{% 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. 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). Add host is a
|
|
dialog over this list (#5291); `add` opens it, carrying a clash's error. #}
|
|
{% 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 %}
|
|
{% set can_add = session.user_role in ['operator', 'admin'] %}
|
|
<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;">
|
|
{% if session.user_role == 'admin' %}
|
|
<a class="btn btn-ghost" href="/plugins/host_agent/fleet/">Agent fleet</a>
|
|
{% endif %}
|
|
{% if can_add %}
|
|
<button type="button" class="btn" onclick="document.getElementById('add-host').showModal()">Add host</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if hosts %}
|
|
<div class="card-flush">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Address</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 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 class="num">
|
|
{% if hs and hs.latency_ms is not none %}
|
|
<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 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>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="card">
|
|
<p style="color:var(--text-muted);">No hosts yet.{% if can_add %} <a href="/hosts/?add=1" onclick="document.getElementById('add-host').showModal();return false;">Summon one to the table.</a>{% endif %}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can_add %}
|
|
{% set v = (add or {}).get('values') or {} %}
|
|
<dialog id="add-host" class="modal" aria-labelledby="add-host-title">
|
|
<h2 id="add-host-title" class="modal-title">Add host</h2>
|
|
{% if add and add.error %}<p class="field-error" role="alert">{{ add.error }}</p>{% endif %}
|
|
<form method="post" action="/hosts/" style="display:grid;gap:0.75rem;">{{ csrf_field() }}
|
|
<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 or '' }}"
|
|
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 or '' }}"
|
|
placeholder="hostname or IP; empty resolves by name" autocomplete="off" class="input">
|
|
</div>
|
|
<p class="field-hint" style="margin:0;">Next, on the host's page: add monitors, install its agent, and link an Ansible target.</p>
|
|
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
|
|
<button type="button" class="btn btn-sm btn-ghost" onclick="this.closest('dialog').close()">Cancel</button>
|
|
<button type="submit" class="btn btn-sm">Add host</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
{% if add is not none %}<script>document.getElementById('add-host').showModal();</script>{% endif %}
|
|
{% endif %}
|
|
{% endblock %}
|