# Host Overview Info Section — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add an always-visible info section to each host card on `/plugins`, showing owner, managers, agent version/type, last packet timestamp, and effective thresholds; move hbc_version/hbc_type out of the os_info accordion. **Architecture:** A new `_build_host_info` module-level helper in `http.py` assembles the info dict from the host object and threshold_checker. A new `GET /api/0/hosts/{hostname}/info` closure inside `serve()` calls it and returns JSON. The `plugins.html` template adds a static placeholder div per host; JS fetches the endpoint on first card expand, caches the result, and renders it. **Tech Stack:** Python/aiohttp (backend), Jinja2 (template), vanilla JS/HTML/CSS (frontend). Tests with pytest and unittest.mock. --- ### Task 1: `_build_host_info` helper — tests first **Files:** - Create: `tests/test_http_host_info.py` - Modify: `hbd/server/http.py` (add module-level helper after `_mask_config_for_api`, around line 128) - [ ] **Step 1: Write the failing tests** Create `tests/test_http_host_info.py`: ```python """Tests for _build_host_info helper in http.py.""" import pytest from unittest.mock import MagicMock from hbd.server.http import _build_host_info class _FakeConn: def __init__(self, lastbeat): self.lastbeat = lastbeat class _FakeHost: def __init__(self, name="myhost", owner=None, managers=None, connections=None, os_data=None): self.name = name self.owner = owner self.managers = managers or [] self.connections = connections or {} self._os_data = os_data def get_latest_plugin_data(self, plugin_name): if plugin_name == "os_info" and self._os_data is not None: return (1234567890.0, self._os_data) return None def test_build_host_info_basic_fields(): host = _FakeHost(owner="alice", managers=["bob", "carol"]) result = _build_host_info(host) assert result["owner"] == "alice" assert result["managers"] == ["bob", "carol"] assert result["hbc_version"] is None assert result["hbc_type"] is None assert result["last_packet"] is None assert result["thresholds"] is None def test_build_host_info_no_owner(): host = _FakeHost() result = _build_host_info(host) assert result["owner"] is None assert result["managers"] == [] def test_build_host_info_reads_hbc_from_os_info(): host = _FakeHost(os_data={"hbc_version": "5.3.0", "hbc_type": "full"}) result = _build_host_info(host) assert result["hbc_version"] == "5.3.0" assert result["hbc_type"] == "full" def test_build_host_info_hbc_none_when_no_os_info(): host = _FakeHost(os_data=None) result = _build_host_info(host) assert result["hbc_version"] is None assert result["hbc_type"] is None def test_build_host_info_last_packet_is_max_lastbeat(): host = _FakeHost(connections={ "IPv4": _FakeConn(1000.0), "IPv6": _FakeConn(2000.0), }) result = _build_host_info(host) assert result["last_packet"] == 2000.0 def test_build_host_info_last_packet_none_when_no_connections(): host = _FakeHost(connections={}) result = _build_host_info(host) assert result["last_packet"] is None def test_build_host_info_thresholds_none_without_checker(): host = _FakeHost() result = _build_host_info(host, threshold_checker=None) assert result["thresholds"] is None def test_build_host_info_thresholds_sorted_by_metric(): from hbd.server.threshold import ThresholdConfig tc_cpu = ThresholdConfig("cpu_monitor.cpu_percent", warning=80.0, critical=95.0) tc_mem = ThresholdConfig("memory_monitor.memory_percent", warning=85.0, critical=98.0) checker = MagicMock() checker.get_thresholds_for_host.return_value = { "memory_monitor.memory_percent": tc_mem, "cpu_monitor.cpu_percent": tc_cpu, } host = _FakeHost() result = _build_host_info(host, threshold_checker=checker) assert result["thresholds"] is not None assert len(result["thresholds"]) == 2 assert result["thresholds"][0]["metric"] == "cpu_monitor.cpu_percent" assert result["thresholds"][0]["warning"] == 80.0 assert result["thresholds"][0]["critical"] == 95.0 assert result["thresholds"][0]["operator"] == ">" assert result["thresholds"][1]["metric"] == "memory_monitor.memory_percent" def test_build_host_info_thresholds_empty_list_when_no_thresholds(): checker = MagicMock() checker.get_thresholds_for_host.return_value = {} host = _FakeHost() result = _build_host_info(host, threshold_checker=checker) assert result["thresholds"] == [] def test_build_host_info_threshold_null_warning_critical(): from hbd.server.threshold import ThresholdConfig tc = ThresholdConfig("rtt.myhost", warning=None, critical=500.0) checker = MagicMock() checker.get_thresholds_for_host.return_value = {"rtt.myhost": tc} host = _FakeHost() result = _build_host_info(host, threshold_checker=checker) assert result["thresholds"][0]["warning"] is None assert result["thresholds"][0]["critical"] == 500.0 ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash pytest tests/test_http_host_info.py -v ``` Expected: `ImportError` or `AttributeError` — `_build_host_info` does not exist yet. - [ ] **Step 3: Implement `_build_host_info` in `hbd/server/http.py`** Insert after `_mask_config_for_api` (around line 128, before `def serve(`): ```python def _build_host_info(host, threshold_checker=None): """Assemble the info payload for GET /api/0/hosts/{hostname}/info.""" hbc_version = None hbc_type = None latest_os = host.get_latest_plugin_data("os_info") if latest_os: _, os_data = latest_os hbc_version = os_data.get("hbc_version") hbc_type = os_data.get("hbc_type") last_packet = None if host.connections: last_packet = max(conn.lastbeat for conn in host.connections.values()) thresholds = None if threshold_checker is not None: raw = threshold_checker.get_thresholds_for_host(host.name) thresholds = sorted( [ { "metric": tc.metric_path, "warning": tc.warning, "critical": tc.critical, "operator": tc.operator.value, } for tc in raw.values() ], key=lambda x: x["metric"], ) return { "owner": getattr(host, "owner", None), "managers": list(getattr(host, "managers", [])), "hbc_version": hbc_version, "hbc_type": hbc_type, "last_packet": last_packet, "thresholds": thresholds, } ``` - [ ] **Step 4: Run tests to confirm they pass** ```bash pytest tests/test_http_host_info.py -v ``` Expected: all 11 tests PASS. - [ ] **Step 5: Commit** ```bash git add tests/test_http_host_info.py hbd/server/http.py git commit -m "feat: add _build_host_info helper for host info endpoint" ``` --- ### Task 2: `api_host_info` route handler **Files:** - Modify: `hbd/server/http.py` - Add `api_host_info` closure inside `serve()` (after `api_host_access_put`, around line 829) - Register route (around line 1271) - [ ] **Step 1: Add `api_host_info` closure inside `serve()`** Insert after `api_host_access_put` (after line 829, before the comment `# User profile page`): ```python # ------------------------------------------------------------------------- # Host info endpoint # ------------------------------------------------------------------------- async def api_host_info(request): """GET /api/0/hosts/{hostname}/info""" user, err = _require_auth(request) if err: return err hostname = request.match_info.get("hostname") if hostname not in hbdclass.Host.hosts: return web.json_response({"error": f"Host '{hostname}' not found"}, status=404) host = hbdclass.Host.hosts[hostname] if not _can_view_host(user, host): return web.json_response({"error": "Forbidden"}, status=403) return web.json_response(_build_host_info(host, threshold_checker=threshold_checker)) ``` - [ ] **Step 2: Register the route** In the route list (around line 1271, after the existing `/api/0/hosts/{hostname}/access` routes): ```python web.get("/api/0/hosts/{hostname}/info", api_host_info), ``` - [ ] **Step 3: Verify the full test suite still passes** ```bash pytest tests/ -q ``` Expected: all tests PASS (no regressions). - [ ] **Step 4: Smoke-test the endpoint manually** (if a dev server is running) ```bash curl -s http://localhost:50004/api/0/hosts//info | python3 -m json.tool ``` Expected: JSON with `owner`, `managers`, `hbc_version`, `hbc_type`, `last_packet`, `thresholds` keys. - [ ] **Step 5: Commit** ```bash git add hbd/server/http.py git commit -m "feat: add GET /api/0/hosts/{hostname}/info endpoint" ``` --- ### Task 3: Info section HTML and CSS in `plugins.html` **Files:** - Modify: `hbd/server/templates/plugins.html` - [ ] **Step 1: Add CSS for the info section** In the `` tag around line 391 and insert before it): ```css /* ── Host info section ──────────────────────────────────────────────────── */ .host-info-section { padding: 12px 16px; background: #fafafa; border-bottom: 1px solid #e0e0e0; font-size: 0.85em; } .info-meta { display: grid; grid-template-columns: max-content 1fr; gap: 3px 14px; margin-bottom: 10px; } .info-label { font-weight: 600; color: #555; white-space: nowrap; } .info-value { color: #222; } .info-thresholds-title { font-weight: 600; color: #555; margin-bottom: 6px; } .info-note { color: #888; font-style: italic; } .info-loading { color: #bbb; font-style: italic; } ``` - [ ] **Step 2: Add info section placeholder to each host card** Inside the host loop, at the very start of `.host-body` (before the `{% set plugin_order %}` line, around line 438): ```html
Loading…
``` The existing `{% set plugin_order %}` line and everything after stays unchanged. Only add the two new lines between `
` and `{% set plugin_order %}`. - [ ] **Step 3: Verify the page still renders without JS errors** Start the dev server and open `/plugins` in a browser. Expand any host card — you should see the "Loading…" italic line above the plugin accordions (it will not be replaced yet, that comes in Task 4). - [ ] **Step 4: Commit** ```bash git add hbd/server/templates/plugins.html git commit -m "feat: add host info section placeholder and CSS to plugins.html" ``` --- ### Task 4: JS — `infoCache`, `fetchHostInfo`, `renderInfoSection` **Files:** - Modify: `hbd/server/templates/plugins.html` (JS `