# Phase 22 — /disks hides hosts without smartctl **Source:** `TODO.md` L13 — "VMs that don't have smartctl enabled disks should not show up. Gateway shouldn't even be a host on the disks page." **Story:** n/a **Context:** `src/dashboards/api.py` (`disks_router()` at line 791 — `/api/disks/snapshot` answering one entry per configured host; the `/api/summary` disks block at lines 640–704 — `disks_hosts` built from `job.state.payload or _disks_stub(...)` at line 640, `fleet_disk_summary(disks_hosts)` at line 693; `_disks_stub()` at line 771 — `smartctl_available: None` for a never-answered host), `src/dashboards/sources/disks.py` (`smartctl_available()` at line 142 — the three-way flag: `True` a disk answered, `False` the agent said the binary is not installed, `None` an agent predating the pass or `--no-smart`), `src/dashboards/web/static/page-disks.js` (the per-host cards + the `fleet_disk_summary` header, hidden below two hosts), `tests/test_api.py` (the `/api/disks/*` pins — "one entry per configured host", the `smartctl_available` three-way, "with and without targets configured", the `fleet_disk_summary` key paths) ## Objective A host whose agent answered `smartctl_available: false` (the binary is not installed — gateway's VM, no smartmontools behind `vda`) is not a host on the `/disks` page at all: it is absent from `/api/disks/snapshot`, from the fleet block and from `/api/summary`'s disks rows, and the fleet header counts only the hosts that are shown. The **three-way** flag itself is unchanged in `sources/disks.py` (the agent still reports it, the `/disks` page's `null`-case why-line still exists for `None` hosts, and an **unreachable** host keeps its dimmed stub row — a dead agent is a different fact from a missing binary, invariant 3). The filter is one API-level decision, so the source, the store and the health records see nothing new. ## Dependencies - `21_page_cleanups` (todo) — both phases touch the `/disks` page markup (task 21.01 removes the page's `#pause` button); landing 21 first keeps this phase's diff confined to the host list. ## Tasks 1. `01_hide_smartctl_false_hosts.md` — the one API-level visibility gate (explicit `False` only) over `/api/disks/snapshot`, `/api/summary`'s disks block and the fleet summary; the pins rewritten 2. `02_verification.md` — browser + API verification against the live fleet: gateway absent, deskwork/driveripper intact, fleet header consistent, screenshots ## Testing & Quality - Unit/integration: `tests/test_api.py` — the rewritten `/api/disks/snapshot` pin (one entry per configured host **whose agent can read its drives** — a `smartctl_available: false` payload is absent, a `None` payload is present, an `unreachable` stub is present), the `/api/summary` disks-block pin (fleet counts over the visible hosts, per-host rows for the visible hosts only), the unchanged `smartctl_available` three-way pin in `sources/disks.py`'s own tests (`tests/test_disks.py` or wherever the flag is pinned — grep `smartctl_available` in `tests/` first) - Coverage: **>90%** on new/modified code - The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the project, not just this phase's) ## Documentation & Consistency - The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands after this phase — fixing every inconsistency found, **including pre-existing ones not caused by this phase**. AGENTS.md's `sources/disks.py` bullet (the three-way flag's `False` arm — "so the page can say *install smartmontools*") and the `/disks` UI paragraph (the `.banner.info` why-line for `false`) are rewritten: the `false` arm's page-side consequence is now *absence*, and the three-way flag keeps its agent-side meaning; the "gateway answers `smartctl_available: false`" fleet fact gains the "and is therefore not a host on the disks page" clause. ## Completion Criteria - [ ] `/api/disks/snapshot` and `/api/summary`'s disks block carry no entry for a host whose agent answered `smartctl_available: false`; a `None` host and an `unreachable` stub both remain (pinned both ways in `tests/test_api.py`) - [ ] the `/disks` page on the live fleet shows deskwork + driveripper only, the fleet header counts 2 hosts, and the per-host cards are unchanged (screenshot in `data/logs/`) - [ ] `uv run pytest` green, coverage not down, `ruff` + `pyright` clean - [ ] `bash .agents/validate.sh` green - [ ] no behavior change in completed work: the agent's SMART pass, the `disks:` job's health record, the stored `disk.*` series, the `None`-case why-line and the unreachable stub are all untouched