feat(hosts): the Hosts list searches, filters by group and status, sorts by column, and shows each host's groups (#5294)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 52s
CI / unit (push) Successful in 1m17s
CI / publish (push) Skipped

#5294 "Hosts list: search, group and status filters; sortable columns; a Groups column".
Ansible inventory groups are Steward's grouping system (operator's ruling): a host's
groups are its linked target's, and membership overlaps, so the list filters by group
rather than sectioning by it.

- Filter bar: search on name or address, a group (or "No group"), a status (Down,
  Agent silent, Up, No monitors). The table swaps in alone and the URL keeps the filters.
- Name, Status, Latency, 30d uptime, CPU and Memory headers sort (sort_th in _macros);
  problems-first stays the default, and a host with no value goes last either way.
- A Groups column of chips, each filtering to its group; shown when any group exists.
- A swap is keyed on HX-Target, so Back to a pushed URL still gets the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 18:53:03 -04:00
co-authored by Claude Opus 5.5
parent a92eb8001b
commit 2990113e10
8 changed files with 423 additions and 85 deletions
+14 -1
View File
@@ -184,11 +184,24 @@ strong, b, th { font-weight: 500; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover td { background: var(--bg-elevated); }
.table .td-actions { text-align: right; white-space: nowrap; }
/* A header that sorts its column (sort_th in _macros.html): reads as the
header it is, with the arrow on the current sort. */
.sort-link { color: inherit; text-decoration: none; }
.sort-link:hover, .sort-active { color: var(--text); }
.sort-arrow { display: inline-block; min-width: 0.9em; text-align: center; }
/* Search and filters above a list; the search box takes the spare width. */
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; }
.filter-bar input[type=search] { flex: 1 1 16rem; width: auto; }
.filter-bar select { width: auto; flex: 0 0 auto; }
/* A small label that links somewhere, such as a group that filters the list. */
.chip { display: inline-block; margin: 0.1rem 0.3rem 0.1rem 0; padding: 0.05rem 0.45rem; font-size: 0.75rem;
color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--fs-radius-sm); white-space: nowrap; }
.chip:hover { color: var(--text); border-color: var(--border-mid); }
/* Forms */
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--text-muted); font-family: var(--fs-font-body); }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
input[type=text], input[type=search], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border-control);
border-radius: 5px; color: var(--text); font-size: 0.9rem; font-family: inherit;
transition: border-color .15s;