refactor(hosts): remove legacy host_agent redirects/paths (no back-compat)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m25s
CI / publish (push) Successful in 39s

Dev-only instance, no bookmarks — per family rule 22, fully remove old paths
instead of shimming them.

- Delete the /plugins/host_agent/ (index) and /plugins/host_agent/settings/
  redirect routes; delete the now-dead host_list.html fleet template.
- Move the remaining management POST routes off /settings/ to /fleet/
  (add-host, rotate-token, delete) — single canonical prefix.
- Repoint real callers to canonical URLs: dashboard widgets (host resources →
  /hosts/, history → /plugins/host_agent/fleet/), the full-metrics page
  breadcrumb + back link (→ the host hub), Settings→Ansible link, and the
  agent panel's curl-install link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 21:20:11 -04:00
parent 7ef1af2184
commit 17c9c875e4
7 changed files with 14 additions and 74 deletions
+3 -18
View File
@@ -372,14 +372,6 @@ async def _fleet_rows(session) -> list[dict]:
return rows return rows
@host_agent_bp.get("/")
@require_role(UserRole.viewer)
async def index():
"""The fleet view folded into the Hosts hub — kept as a redirect so old
links/widgets don't 404."""
return redirect("/hosts/")
@host_agent_bp.get("/widget") @host_agent_bp.get("/widget")
async def widget_table(): async def widget_table():
"""Fleet-glance dashboard widget: one row per monitored host, latest metrics.""" """Fleet-glance dashboard widget: one row per monitored host, latest metrics."""
@@ -568,13 +560,6 @@ async def host_panel(host_id: str):
) )
@host_agent_bp.get("/settings/")
@require_role(UserRole.admin)
async def settings_redirect():
"""Old management URL — agent management moved into the Hosts section."""
return redirect(url_for("host_agent.fleet"))
@host_agent_bp.get("/fleet/") @host_agent_bp.get("/fleet/")
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def fleet(): async def fleet():
@@ -618,7 +603,7 @@ async def fleet():
) )
@host_agent_bp.post("/settings/add-host") @host_agent_bp.post("/fleet/add-host")
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def add_host(): async def add_host():
form = await request.form form = await request.form
@@ -650,7 +635,7 @@ async def add_host():
return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}") return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}")
@host_agent_bp.post("/settings/<host_id>/rotate-token") @host_agent_bp.post("/fleet/<host_id>/rotate-token")
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def rotate_token(host_id: str): async def rotate_token(host_id: str):
async with current_app.db_sessionmaker() as session: async with current_app.db_sessionmaker() as session:
@@ -666,7 +651,7 @@ async def rotate_token(host_id: str):
return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}") return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}")
@host_agent_bp.post("/settings/<host_id>/delete") @host_agent_bp.post("/fleet/<host_id>/delete")
@require_role(UserRole.admin) @require_role(UserRole.admin)
async def delete_registration(host_id: str): async def delete_registration(host_id: str):
async with current_app.db_sessionmaker() as session: async with current_app.db_sessionmaker() as session:
@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "_macros.html" import crumbs %} {% from "_macros.html" import crumbs %}
{% block title %}{{ host.name }} — Host Agent — Steward{% endblock %} {% block title %}{{ host.name }} — Host Agent — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Host Agents", "/plugins/host_agent/"), (host.name, "")]) }}{% endblock %} {% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Metrics", "")]) }}{% endblock %}
{% macro fmt_bps(v) %} {% macro fmt_bps(v) %}
{%- if v is none -%}— {%- if v is none -%}—
@@ -27,7 +27,7 @@
{% block content %} {% block content %}
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:0.75rem;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:0.75rem;">
<div style="display:flex;align-items:center;gap:0.6rem;"> <div style="display:flex;align-items:center;gap:0.6rem;">
<a href="/plugins/host_agent/" class="btn btn-ghost btn-sm">Fleet</a> <a href="/hosts/{{ host.id }}" class="btn btn-ghost btn-sm">Host</a>
<h1 class="page-title" style="margin-bottom:0;">{{ host.name }}</h1> <h1 class="page-title" style="margin-bottom:0;">{{ host.name }}</h1>
{% if stale %}<span class="badge badge-red">stale</span>{% else %}<span class="badge badge-green">live</span>{% endif %} {% if stale %}<span class="badge badge-red">stale</span>{% else %}<span class="badge badge-green">live</span>{% endif %}
</div> </div>
@@ -1,45 +0,0 @@
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}Host Agents — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Dashboard", "/"), ("Host Agents", "")]) }}{% endblock %}
{% block content %}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;gap:1rem;flex-wrap:wrap;">
<h1 class="page-title" style="margin-bottom:0;">Host Agents</h1>
{% if session.user_role == 'admin' %}
<a class="btn btn-ghost" href="/plugins/host_agent/settings/">Manage agents →</a>
{% endif %}
</div>
{% if not rows %}
<div class="card empty">
No hosts are reporting agent metrics yet.
{% if session.user_role == 'admin' %}
Add one from <a href="/plugins/host_agent/settings/">Host Agent settings</a> and run the install command on the target.
{% else %}
Ask an admin to install the agent on a host.
{% endif %}
</div>
{% else %}
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:1rem;">
{% for r in rows %}
<a href="/plugins/host_agent/{{ r.host.id }}/" class="card" style="display:block;margin-bottom:0;color:inherit;">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.7rem;">
<span class="dot {% if r.stale %}dot-dim{% elif (r.cpu_pct or 0) >= 90 or (r.mem_used_pct or 0) >= 90 or (r.disk_worst or 0) >= 90 %}dot-warn{% else %}dot-up{% endif %}"></span>
<span style="font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ r.host.name }}</span>
{% if r.stale %}<span class="badge badge-dim">stale</span>{% endif %}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.45rem 0.9rem;font-size:0.84rem;font-variant-numeric:tabular-nums;">
<span><span style="color:var(--text-dim);">CPU</span> {% if r.cpu_pct is not none %}{{ "%.0f"|format(r.cpu_pct) }}%{% else %}—{% endif %}</span>
<span><span style="color:var(--text-dim);">Mem</span> {% if r.mem_used_pct is not none %}{{ "%.0f"|format(r.mem_used_pct) }}%{% else %}—{% endif %}</span>
<span><span style="color:var(--text-dim);">Disk</span> {% if r.disk_worst is not none %}{{ "%.0f"|format(r.disk_worst) }}%{% else %}—{% endif %}</span>
<span><span style="color:var(--text-dim);">Load</span> {% if r.load_1m is not none %}{{ "%.2f"|format(r.load_1m) }}{% else %}—{% endif %}</span>
{% if r.temp_max is not none %}<span><span style="color:var(--text-dim);">Temp</span> {{ "%.0f"|format(r.temp_max) }}°C</span>{% endif %}
</div>
<div style="margin-top:0.6rem;font-size:0.72rem;color:var(--text-dim);">
last seen {{ r.reg.last_seen_at.strftime("%Y-%m-%d %H:%M:%S") ~ " UTC" if r.reg.last_seen_at else "never" }}
</div>
</a>
{% endfor %}
</div>
{% endif %}
{% endblock %}
+3 -3
View File
@@ -37,11 +37,11 @@
<button type="submit" class="btn btn-sm" title="Refresh agent.py + restart (token preserved)">Update agent</button> <button type="submit" class="btn btn-sm" title="Refresh agent.py + restart (token preserved)">Update agent</button>
</form> </form>
{% endif %} {% endif %}
<form method="post" action="/plugins/host_agent/settings/{{ host.id }}/rotate-token" style="margin:0;" <form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/rotate-token" style="margin:0;"
onsubmit="return confirm('Rotate token? The agent stops reporting until reinstalled/updated with the new token.');"> onsubmit="return confirm('Rotate token? The agent stops reporting until reinstalled/updated with the new token.');">
<button type="submit" class="btn btn-sm btn-ghost">Rotate token</button> <button type="submit" class="btn btn-sm btn-ghost">Rotate token</button>
</form> </form>
<form method="post" action="/plugins/host_agent/settings/{{ host.id }}/delete" style="margin:0;" <form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0;"
onsubmit="return confirm('Remove this agent registration? Metrics stop until re-registered.');"> onsubmit="return confirm('Remove this agent registration? Metrics stop until re-registered.');">
<button type="submit" class="btn btn-sm btn-danger">Remove</button> <button type="submit" class="btn btn-sm btn-danger">Remove</button>
</form> </form>
@@ -65,7 +65,7 @@
{% elif not ansible_available %} {% elif not ansible_available %}
<p style="color:var(--text-dim);font-size:0.85rem;"> <p style="color:var(--text-dim);font-size:0.85rem;">
Ansible isn't available, so the agent can't be deployed from here. Install it manually with the Ansible isn't available, so the agent can't be deployed from here. Install it manually with the
<a href="/plugins/host_agent/settings/">curl install command</a>. <a href="/plugins/host_agent/fleet/">curl install command</a>.
</p> </p>
{% elif not managed_key_set %} {% elif not managed_key_set %}
@@ -23,7 +23,7 @@
{% endif %} {% endif %}
<div class="card"> <div class="card">
<form method="post" action="/plugins/host_agent/settings/add-host" <form method="post" action="/plugins/host_agent/fleet/add-host"
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;"> style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;"> <div class="form-group" style="margin-bottom:0;">
<label>Host name</label> <label>Host name</label>
@@ -155,12 +155,12 @@
<td>{{ item.reg.distro or "—" }}</td> <td>{{ item.reg.distro or "—" }}</td>
<td>{{ item.reg.last_seen_at.strftime("%Y-%m-%d %H:%M:%S") if item.reg.last_seen_at else "never" }}</td> <td>{{ item.reg.last_seen_at.strftime("%Y-%m-%d %H:%M:%S") if item.reg.last_seen_at else "never" }}</td>
<td class="td-actions"> <td class="td-actions">
<form method="post" action="/plugins/host_agent/settings/{{ item.reg.host_id }}/rotate-token" <form method="post" action="/plugins/host_agent/fleet/{{ item.reg.host_id }}/rotate-token"
style="display:inline;" style="display:inline;"
onsubmit="return confirm('Rotate the token for this host? The existing agent will stop working until reinstalled with the new token.');"> onsubmit="return confirm('Rotate the token for this host? The existing agent will stop working until reinstalled with the new token.');">
<button type="submit" class="btn btn-ghost btn-sm">Rotate token</button> <button type="submit" class="btn btn-ghost btn-sm">Rotate token</button>
</form> </form>
<form method="post" action="/plugins/host_agent/settings/{{ item.reg.host_id }}/delete" <form method="post" action="/plugins/host_agent/fleet/{{ item.reg.host_id }}/delete"
style="display:inline;" style="display:inline;"
onsubmit="return confirm('Delete this host registration? The agent will be unable to report metrics until re-registered.');"> onsubmit="return confirm('Delete this host registration? The agent will be unable to report metrics until re-registered.');">
<button type="submit" class="btn btn-danger btn-sm">Delete</button> <button type="submit" class="btn btn-danger btn-sm">Delete</button>
+2 -2
View File
@@ -262,7 +262,7 @@ WIDGET_REGISTRY: dict[str, dict] = {
"label": "Host Agent — Resources", "label": "Host Agent — Resources",
"description": "Fleet view: CPU, memory, disk, and load per monitored host", "description": "Fleet view: CPU, memory, disk, and load per monitored host",
"hx_url": "/plugins/host_agent/widget", "hx_url": "/plugins/host_agent/widget",
"detail_url": "/plugins/host_agent/", "detail_url": "/hosts/",
"plugin": "host_agent", "plugin": "host_agent",
"poll": True, "poll": True,
"params": [], "params": [],
@@ -272,7 +272,7 @@ WIDGET_REGISTRY: dict[str, dict] = {
"label": "Host Agent — History", "label": "Host Agent — History",
"description": "CPU/memory/disk history for one host", "description": "CPU/memory/disk history for one host",
"hx_url": "/plugins/host_agent/widget/history", "hx_url": "/plugins/host_agent/widget/history",
"detail_url": "/plugins/host_agent/settings/", "detail_url": "/plugins/host_agent/fleet/",
"plugin": "host_agent", "plugin": "host_agent",
"poll": True, "poll": True,
"params": [ "params": [
+1 -1
View File
@@ -29,7 +29,7 @@
Steward's reusable Ansible identity. Provisioning installs the <strong>public</strong> Steward's reusable Ansible identity. Provisioning installs the <strong>public</strong>
key into each host's <code>{{ ssh_user }}</code> account; steady-state runs authenticate key into each host's <code>{{ ssh_user }}</code> account; steady-state runs authenticate
with the private half (stored encrypted). Generate it once, then provision hosts from with the private half (stored encrypted). Generate it once, then provision hosts from
<a href="/plugins/host_agent/settings/">Host Agents</a>. <a href="/plugins/host_agent/fleet/">Host Agents</a>.
</p> </p>
{% if ssh_public_key %} {% if ssh_public_key %}
<div class="form-group"> <div class="form-group">