feat(hosts): Phase 2+3 — agent column, fold fleet + management into Hosts
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 2m16s
CI / publish (push) Successful in 54s

- Hosts list: new Agent column (latest CPU/mem read from the generic
  PluginMetric table — no host_agent import) + an admin "Agent fleet" button.
- /plugins/host_agent/ (old fleet page) now redirects to /hosts/ (folded into
  the hub; kept as a redirect so widgets/links don't 404).
- Agent management moved off the "settings" URL: the management page is now
  /plugins/host_agent/fleet/ ("Agent fleet" — bulk provision/install/update +
  registrations + curl install), reachable from the Hosts list. Old
  /plugins/host_agent/settings/ redirects there. Per-host management lives on
  the host detail page; this page is now explicitly the bulk/fleet view.

Milestone 70 phases 2-3. Phase 4 (plugins capability/integration split + nav
cleanup) next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 20:51:33 -04:00
parent 8bdf07f709
commit f29255039d
4 changed files with 77 additions and 12 deletions
+14 -8
View File
@@ -375,10 +375,9 @@ async def _fleet_rows(session) -> list[dict]:
@host_agent_bp.get("/")
@require_role(UserRole.viewer)
async def index():
"""Fleet overview page — cards per host linking to the detail view."""
async with current_app.db_sessionmaker() as session:
rows = await _fleet_rows(session)
return await render_template("host_list.html", rows=rows)
"""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")
@@ -571,7 +570,14 @@ async def host_panel(host_id: str):
@host_agent_bp.get("/settings/")
@require_role(UserRole.admin)
async def settings_list():
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/")
@require_role(UserRole.admin)
async def fleet():
from steward.core.capabilities import has_capability
from steward.models.ansible_inventory import AnsibleTarget, AnsibleGroup
@@ -641,7 +647,7 @@ async def add_host():
await session.commit()
host_id = host.id
return redirect(url_for("host_agent.settings_list") + 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")
@@ -657,7 +663,7 @@ async def rotate_token(host_id: str):
reg.token_hash = hashed
reg.token_created_at = datetime.now(timezone.utc)
await session.commit()
return redirect(url_for("host_agent.settings_list") + 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")
@@ -670,7 +676,7 @@ async def delete_registration(host_id: str):
if reg is not None:
await session.delete(reg)
await session.commit()
return redirect(url_for("host_agent.settings_list"))
return redirect(url_for("host_agent.fleet"))
# ── Deploy via Ansible (plugin↔core synergy via the capability registry) ──────
@@ -1,9 +1,16 @@
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% block title %}Host Agent — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Host Agents", "/plugins/host_agent/"), ("Settings", "")]) }}{% endblock %}
{% block title %}Agent fleet — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), ("Agent fleet", "")]) }}{% endblock %}
{% block content %}
<h1 class="page-title">Host Agent — Registered Hosts</h1>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;gap:1rem;flex-wrap:wrap;">
<h1 class="page-title" style="margin-bottom:0;">Agent fleet</h1>
<a href="/hosts/" class="btn btn-ghost btn-sm">← Hosts</a>
</div>
<p style="color:var(--text-muted);font-size:0.84rem;margin:-0.4rem 0 1.25rem;">
Bulk agent operations across your inventory. For a single host, use its
<a href="/hosts/">host page</a> — provisioning and metrics live there.
</p>
{% if new_token %}
<div class="card" style="background:var(--accent-bg);">
@@ -58,7 +65,7 @@
No managed SSH key yet — Steward needs one to log into hosts as <code>steward</code>.
</span>
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">
<input type="hidden" name="next" value="/plugins/host_agent/settings/">
<input type="hidden" name="next" value="/plugins/host_agent/fleet/">
<button type="submit" class="btn btn-sm">Generate managed key</button>
</form>
</div>