phase: 22_disks_hide_unconfigured
All green. Final report: **Verified (phase 22 final pass — both task files already in `complete/`)** - `api._disk_visible` (`is not False`) is the single gate, applied to the assembled host list in `/api/disks/snapshot` **and** `/api/summary`'s disks block before the fleet sum and the rows; `sources/disks.py`, `page-disks.js`, `poller.py`, `store.py` and `alerts.py` are byte-unchanged (`git diff --stat`: only `api.py`, `test_api.py`, docs). - Re-ran the live browser pass `data/logs/22-task02-disks-live.cjs` against the running dev server: **34/34 PASS** — no gateway card/why-line, `2/2 hosts answering`, fleet header reads the API's `2 of 2` / `17-17-0-1`, deskwork `nvme0n1` OK 47 °C/19 202 h/11 %, driveripper 16 live cards + `13 uncorrectable` chip, both aria-labelled plots, `/` still 3 fleet rows. Screenshots + `22-task02-api.json` refreshed. - `./scripts/dev-server.sh status`: all **nine sources `ok`**, `disks:gateway` included; live `/api/events?source=disks` still carries gateway transitions (10 of 45), proving the README/AGENTS claim the gate hides a row and not the job's history. **Completion criteria** - `false` host absent from both endpoints, `None` + `unreachable` stub present, fleet ≡ row count: **PASS** (6 pins in `tests/test_api.py`, incl. the single-hidden-host = 200-with-empty-list leg). - Live `/disks` = deskwork + driveripper, header 2 hosts, cards unchanged, screenshot in `data/logs/`: **PASS** (re-run above). - `uv run pytest` → **1477 passed, 15 skipped**; `--cov --cov-fail-under=94` → **98.94 %** (`api.py` 100 %); `ruff check src tests agent` → all passed; `pyright` → 0 errors; `bash .agents/validate.sh` → **EXIT 0**. `uv run pytest -m live --live` → **11 passed**. - No behaviour change in completed work: **PASS** (unchanged files above; live numbers match the pre-phase fleet record). **Docs** — README: `/disks` page-table row + the agent SMART paragraph now distinguish "cannot read drives" (dimmed cards kept) from "no smartctl" (host absent from the page/API lists). AGENTS.md: hermetic tally 1471→**1477**, and the `collectors.py`/`api.py` passages now note the `false` arm never reaches the `/disks` why-line while an overview alert from a hidden host's `disks` job still names it. Everything else (routes vs served table, env table vs `config.py`, commands, scripts/fixtures/tests references, masthead/POLL_MS/threshold/retention claims) **verified consistent**. **Deviations** — `NOTES.md` left empty by the task runs (both notes are tracked: mTLS → phase 24 `todo/`, the admin-links bug → fixed in phase 20 task 04); `/disks`' `#hosts-empty` copy left as pinned markup (it already names smartmontools + the disk group, so an all-hidden fleet is still told the fix); validate's advisory `fastapi 0.153.0 → 0.156.4` untouched (no dependency changed this phase). No live fleet writes of any kind. **Next pending phase:** `23_llm_rate_responsiveness`.
This commit is contained in:
@@ -289,7 +289,9 @@ Responsibilities, one line each:
|
||||
literal `None` means *no timeout at all*). Agent `errors`
|
||||
become the payload's `degraded` **except** the `smart:` add-on lines — a host without
|
||||
smartmontools or device read access is unconfigured, not broken (invariant 3); they stay
|
||||
in `agent.errors` for the `/disks` page to explain.
|
||||
in `agent.errors` for the `/disks` page to explain — which since phase 22 is only the
|
||||
device-read case, because a host whose agent said `smartctl` is *missing* is not listed on
|
||||
that page at all (`api._disk_visible`).
|
||||
- `sources/host.py` — snapshot → payload + samples; reboot detection; GPU series per card
|
||||
(`gpu.busy_pct@<index>`). `agent_tls_fields()` is the phase-15 passthrough: a TLS agent's
|
||||
`tls`/`bundle_rev` ride into the payload's existing `agent` block, and a token-mode snapshot
|
||||
@@ -299,9 +301,12 @@ Responsibilities, one line each:
|
||||
(phase 09): one row per whole-`/sys/block` disk (never a partition, never a mount), the
|
||||
agent's `smart` block carried through verbatim, `health` the drive's own word normalised to
|
||||
`ok`/`failed`/`None`, and `smartctl_available` three-way on purpose — `True` (a disk
|
||||
answered), `False` (the agent said the binary is not installed, so the page can say *install
|
||||
smartmontools*), `None` (an agent predating the pass, or `--no-smart`: nobody knows, so
|
||||
`n/a` plus the agent's own `smart_errors` line and never a guessed verdict). **Nothing here
|
||||
answered), `False` (the agent said the binary is not installed), `None` (an agent predating
|
||||
the pass, or `--no-smart`: nobody knows, so `n/a` plus the agent's own `smart_errors` line
|
||||
and never a guessed verdict). Since phase 22 the flag's *page-side* consequence is not only
|
||||
the why-line: `api._disk_visible` reads `False` as "not a host on `/disks` at all", while
|
||||
`None` keeps its dimmed row — the computation here is unchanged and the agent still reports
|
||||
all three arms whatever the API decides to list. **Nothing here
|
||||
is a rate**: power-on hours and the sector counters are stored counters-as-reported, so the
|
||||
source takes no `RateEstimator` and its payload carries no `window_seconds` (rule 2 — a rate
|
||||
with no estimator, no `Δ<window>` label and no `source` string is an invented number).
|
||||
@@ -606,14 +611,27 @@ Responsibilities, one line each:
|
||||
"fix" that by hiding the event). Neither fact appears on `/api/health` — which is public — or
|
||||
`/api/config`. `/api/config` is a redaction boundary.
|
||||
`/api/disks/*` (phase 09) is the drive-health pair: `/snapshot` answers one entry per
|
||||
configured host (disk rows + `smart` + `smartctl_available` + per-row `severity` + a
|
||||
`fleet` block summed by the API, with a configured-but-never-answered host as an
|
||||
`unreachable` stub whose counts are `None`, not `0`), and `/history` serves the
|
||||
configured host **with drives to show** (disk rows + `smart` + `smartctl_available` +
|
||||
per-row `severity` + a `fleet` block summed by the API, with a configured-but-never-answered
|
||||
host as an `unreachable` stub whose counts are `None`, not `0`), and `/history` serves the
|
||||
`disk.*@dev` series with unknown names answering as empty series like the other two
|
||||
history endpoints. The configured gate runs **before** anything that could reach the
|
||||
network — `HOST_TARGETS` empty is `404 {"configured": false}` with no client, no fetch and
|
||||
no `disks` job in the hub at all (invariant 3), and `tests/test_api.py` proves it with
|
||||
spies on `httpx.AsyncClient.get` and `collectors.fetch_snapshot`. The new surface grows no
|
||||
spies on `httpx.AsyncClient.get` and `collectors.fetch_snapshot`. Configured-ness and
|
||||
visibility are two different gates, and only one helper decides the second:
|
||||
`api._disk_visible(host)` drops a host whose payload's `smartctl_available` is **explicitly**
|
||||
`False` ("no smartmontools, no drives to report" — the VM case) from `hosts` and therefore
|
||||
from `fleet` (`fleet_disk_summary` sums whatever it is handed, so `hosts_total` is the row
|
||||
count), while `True`, `None` and an `unreachable` stub all stay — an agent that cannot say is
|
||||
not an agent that said *no*, and a dead agent must not read as "never configured"
|
||||
(invariant 3). The check is `is not False`, not falsy, and it runs in `/api/summary`'s disks
|
||||
block too, before both the fleet sum and the per-host rows, so the two endpoints cannot list
|
||||
different machines; `states` still names every `disks:<host>` job (hidden included, still
|
||||
polling, still `ok`) and the source, the store and the health records see no change at all —
|
||||
which is also why an overview alert from a hidden host's `disks` job still names that host
|
||||
(`alerts.py` reads job states, not this list): hiding a card must never hide a fault
|
||||
(phase 22). The new surface grows no
|
||||
write of its own — a fourth dashboard must not quietly become a second one, so the app's
|
||||
only POST that **dials an upstream** stays `/api/ups/command` (invariant 1, pinned by enumerating
|
||||
every served route's verbs and sorting the writes into fleet / session / certificate-authority
|
||||
@@ -635,7 +653,12 @@ Responsibilities, one line each:
|
||||
`health_failed`/`alarm_disks`/`smartctl_available`/`failed_devices`), so a notifier learns
|
||||
*which* drive the hardware called `FAILED` without a second call, and
|
||||
`tests/test_api.py` pins every one of those key paths, with and without targets configured,
|
||||
so retiring a panel can never shrink the payload. A configured
|
||||
so retiring a panel can never shrink the payload. Since phase 22 those rows are the **visible**
|
||||
hosts — the same `_disk_visible` predicate `/api/disks/snapshot` applies, over the same
|
||||
entries, before the fleet sum — so `disks.fleet` equals `/api/disks/snapshot`'s `fleet` and a
|
||||
host with no smartmontools is missing from both, while `system.hosts` still lists it (a missing
|
||||
binary is a fact about the drive page and nothing else) and `disks.states` still lists its job.
|
||||
A configured
|
||||
job with no payload yet is listed as a stub (`unreachable: true`, every value `null`) in
|
||||
`system.hosts`/`llm.engines` exactly as `/api/system/snapshot` does, so `hosts_total`
|
||||
counts the machines there are and `hosts_live` the ones answering.
|
||||
@@ -1002,7 +1025,13 @@ That scheme is the history API's key — treat it as a public interface.
|
||||
odometer, so that figure is a capture date, not a fact about the drive), 11 % of rated
|
||||
wear — so the wear bar and the alarm chips are drawn from live hardware, not only fixtures.
|
||||
`gateway` answers `smartctl_available: false` (no smartmontools in its VM: the *install it*
|
||||
half of the three-way flag), and **`driveripper` is the fourth case the flag was built for:
|
||||
half of the three-way flag — and, since phase 22, the arm that keeps it off the `/disks` page
|
||||
entirely, so `/api/disks/snapshot` there lists deskwork + driveripper and `fleet.hosts_total`
|
||||
is 2 while `host:gateway`/`disks:gateway` both stay `ok` — re-verified live read-only on
|
||||
2026-10-21, where the two cards still draw their pre-phase numbers (`deskwork nvme0n1` 48 °C /
|
||||
19 201 h / 11 %, `driveripper` 16 drives with the `nvme5n1` `13 uncorrectable` chip, `/`
|
||||
unchanged at three fleet rows; `data/logs/NOTES-22-disks-live.md`), and **`driveripper` is the fourth
|
||||
case the flag was built for:
|
||||
`smartctl_available: true` with almost no numbers.** Its agent answers (16 whole-disk rows,
|
||||
`nvme0n1`…`nvme7n1` + `sda`…`sdh`) and `smart` is non-null for the 8 NVMes, but every one of
|
||||
them carries only identity — `health`, `temp_c`, `power_on_hours`, `percentage_used` and the
|
||||
@@ -1047,9 +1076,16 @@ That scheme is the history API's key — treat it as a public interface.
|
||||
nothing but are pure noise, while with the grant they are 16 drives' worth of temperature,
|
||||
hours and sector counters). **The agent and the `disk` group are both in place on
|
||||
`driveripper`** — the `disk` group alone is not enough on kernel 7.2 (the passthrough gate, see above), so the unit additionally runs as root with the two gate capabilities bounded (gap 10) — all 16 drives now read live. `gateway` needs smartmontools first and will still read no SMART
|
||||
after that, being a VM with no physical disk behind `vda`. A host that cannot read its drives
|
||||
renders the designed degradation: one dimmed card per physical drive, every number `n/a`, and
|
||||
the one-line why.
|
||||
after that, being a VM with no physical disk behind `vda` — and it is the host phase 22's
|
||||
visibility gate exists for: its `smartctl_available: false` means `/disks` draws **no gateway
|
||||
card at all** and `/api/disks/snapshot` lists two hosts, not three — while its `disks:gateway`
|
||||
job still polls, still reads `ok`, and its transitions still reach that page's drive-events
|
||||
feed, because the gate hides a host row and not the job's history. That is a different
|
||||
designed degradation from a host that cannot *read* its drives for any other reason (a denied
|
||||
device, `--no-smart`, an agent predating the pass): that one keeps its dimmed cards, every
|
||||
number `n/a`, and the one-line why — **no host here is in that `None` arm today** (all three
|
||||
agents post-date the SMART pass), so the arm that stays visible is proven by
|
||||
`tests/test_api.py`'s pins and by nothing on the fleet.
|
||||
`tests/fixtures/smartctl_reference_*.txt` stay as what they are: **reference shapes** (labels
|
||||
verbatim from the deployed binary, values synthetic), kept beside the real captures so the
|
||||
synthetic edge cases stay pinned. Re-record the real ones with
|
||||
@@ -1074,7 +1110,7 @@ set -a; . ./.env.dev; set +a # dev dials the fleet the prod way, no
|
||||
./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health
|
||||
./scripts/dev-server.sh logs
|
||||
|
||||
uv run pytest # hermetic: 1471 passed + 15 skipped (11 live,
|
||||
uv run pytest # hermetic: 1477 passed + 15 skipped (11 live,
|
||||
# 4 store_perf), no network
|
||||
uv run pytest --cov # coverage gate (98.94%; floor 94%)
|
||||
uv run pytest -m live --live # opt-in: real fleet / engines / upsd
|
||||
@@ -1366,12 +1402,17 @@ counters are **`.chip.fail` alarm chips that exist only while non-zero** (the `/
|
||||
rule, and why `severity.py` rates no counter): a drive at zero draws no chip row at all, because a
|
||||
permanent "0 pending sectors" line trains the eye to ignore it and a green one would call a quiet
|
||||
drive an achievement. Per host the three-way `smartctl_available` decides one `.banner.info`
|
||||
why-line and nothing else — `false` says *install smartmontools and grant the disk group* (the
|
||||
agent said so, so the page may), `null` prints the agent's own `smart_errors` line or names both
|
||||
fixes without claiming to know which half is missing, and an agent that is not answering says
|
||||
that instead — and in every case the drive grid takes the shared `.stale` dim while the host card
|
||||
stays uncoloured (invariant 3: a missing binary is unconfigured, not broken, and a red panel here
|
||||
would train the operator to ignore red on the drives that do report). Nothing on the page is per
|
||||
why-line and nothing else — `null` prints the agent's own `smart_errors` line or names both fixes
|
||||
without claiming to know which half is missing, and an agent that is not answering says that
|
||||
instead — and in either case the drive grid takes the shared `.stale` dim while the host card
|
||||
stays uncoloured (invariant 3: unreadable drives are unconfigured, not broken, and a red panel
|
||||
here would train the operator to ignore red on the drives that do report). The `false` arm — the
|
||||
agent said `smartctl` is not installed — no longer reaches the page at all: `api._disk_visible`
|
||||
drops that host from `/api/disks/snapshot`, from the fleet sums and from `/api/summary`'s disks
|
||||
rows (phase 22), because a card whose only content is *install smartmontools and grant the disk
|
||||
group* is not a host on a drive page; `whyLine()`'s `false` sentence stays as the renderer for a
|
||||
payload that says it, so relaxing the gate later cannot turn those hosts into silent blank cards.
|
||||
Nothing on the page is per
|
||||
partition or per mount: the rows are the agent's whole-`/sys/block` list, and a filesystem field
|
||||
naming its way into `page-disks.js` fails a test. The fleet header renders
|
||||
`fleet_disk_summary()` — hidden below two hosts (one box's sums duplicate its own cards) and
|
||||
|
||||
Reference in New Issue
Block a user