The host knew about every one of these faults and had no way to say so #253

Merged
enricobuehler merged 1 commits from worktree-console-diagnostics into main 2026-08-15 15:56:22 +00:00
Owner

Implements design/web-console-diagnostics.md, WP1–WP3 (the v1). WP4 (live checks + SSE) and WP5 (instant helpers) stay deferred, as that plan sequences them.

The .181 incident is the type specimen. preflight_takeover_privilege() is the most careful probe in the repo — four applicability gates, and it even separates user-database membership from the running process's supplementary groups — and it spends all of that care on one WARN log line. A console-driven update never shows scriptlet stderr, so the operator's only symptom was a black screen on every connect. The class of bug is the host knows, and the person operating it has no way to find out.

Host

diagnostics.rs holds the HostCheck model and a registry (probes in, cached verdicts out; POST /refresh re-runs them). inapplicable is a first-class status rather than an absent row, so the page can answer "why isn't this check relevant here?" instead of silently hiding it. diagnostics/catalog.rs maps verdicts to wire checks and owns every user-visible string.

GET /api/v1/diagnostics + POST /api/v1/diagnostics/refresh are admin lane only. Neither allowlist in auth.rs is touched: both are opt-in, and these verdicts carry the host user's name, its group layout and device-node state.

Probes stay in their owning crates and export plain verdict enums; the host does the mapping. No reverse dependency — pf-inject/pf-vdisplay never learn about host types.

  • pf-vdisplay: preflight_takeover_privilege() now logs from takeover_privilege_verdict(). The WARN line is unchanged, deliberately: headless operators read logs, not consoles, so this moves where the verdict goes, not what it says.
  • pf-inject: uinput_probe() keeps the errno pen_supported() throws away, so "you are not in the input group" (EACCES) and "the module was never installed" (ENOENT) stop looking identical — they need opposite remedies. vhci_probe() reports device facts only; who to blame is the host's question, because only the user database can answer it.

Two distinctions the catalog refuses to collapse:

  1. User-database membership vs this process's groups. usermod -aG satisfies the first immediately and the second not until the next login. Collapsing them produces the most maddening support state there is — "I already added myself!" — which nothing in the logs distinguishes today. It gets its own remedy: log out, no command to run.
  2. usermod does not stick on an atomic OS. On the Universal Blue images the remedy is ujust add-user-to-input-group, matched on the OS chain's leaf, never on the fedora family token — plain Fedora Workstation is mutable and does want usermod.

Console

The dashboard gets an AttentionCard that renders nothing at all on a healthy host (ConflictsCard's rule), shows at most the 3 worst checks, and links onward rather than explaining.

Its badge says the severity, not the status. The colour already encodes severity, so a badge reading "Failing" on both a red and an amber row leaves the difference carried by colour alone — the thing the pin_pending precedent exists to prevent. Caught on glass, not in the diff.

The Logs page becomes Troubleshooting: checks above the log stream, because when the checks are green and something is still broken the log is the natural next step. The route stays /logs — bookmarks and deep links outlive a label.

A check id this console has never heard of still renders, from the host's English summary/impact/remedy.text. That is what makes console N paired with host N+1 survivable, and it is enforced as a test rather than left as a convention.

Reviewer notes — deliberate deviations from the plan

  • The registry is a process-global OnceLock, not an AppState/MgmtState field as §2 sketched. It matches hooks::store() / detect::snapshot(), lets the handlers take no State extractor, and leaves six call sites untouched.
  • Timestamps are u64 unix seconds named *_unix (house style, cf. started_unix/fetched_at) rather than the design sketch's RFC-3339 — there is no date crate in the host.
  • The console localizes check names + chrome only, rendering the host's situational prose verbatim. One check has up to four failure shapes, so per-id _summary/_impact/_remedy keys would duplicate ~20 sentences into a package that versions independently — the exact drift §2.3 warns about. Full localization wants a shape discriminator on the wire first.

Two traps the plan missed, both build-breaking and both handled here: docs-site/public/openapi.json is a second checked-in copy that nothing tests, and every_route_is_classified_for_the_plugin_and_cert_lanes fails until each new route gets an explicit lane row (diagnostics is false, false on both).

Verification

cargo clippy -D warnings and cargo fmt --all --check clean; 12 diagnostics unit tests and 48 mgmt handler tests green, including the openapi drift test — api/openapi.json and the ungated docs-site copy are both regenerated. Web: build, lint, bun test server/, storybook, and the new stories shot in both themes with the theme flip verified rather than assumed.

Not yet on glass: the real .181 box (all three vhci states), .138 for the unpackaged-helper path, and the old-console/new-host pairing legs.

Implements `design/web-console-diagnostics.md`, **WP1–WP3** (the v1). WP4 (live checks + SSE) and WP5 (instant helpers) stay deferred, as that plan sequences them. The `.181` incident is the type specimen. `preflight_takeover_privilege()` is the most careful probe in the repo — four applicability gates, and it even separates user-database membership from the running process's supplementary groups — and it spends all of that care on **one WARN log line**. A console-driven update never shows scriptlet stderr, so the operator's only symptom was a black screen on every connect. The class of bug is *the host knows, and the person operating it has no way to find out*. ## Host `diagnostics.rs` holds the `HostCheck` model and a registry (probes in, cached verdicts out; `POST /refresh` re-runs them). `inapplicable` is a first-class status rather than an absent row, so the page can answer "why isn't this check relevant here?" instead of silently hiding it. `diagnostics/catalog.rs` maps verdicts to wire checks and owns every user-visible string. `GET /api/v1/diagnostics` + `POST /api/v1/diagnostics/refresh` are **admin lane only**. Neither allowlist in `auth.rs` is touched: both are opt-in, and these verdicts carry the host user's name, its group layout and device-node state. Probes stay in their owning crates and export plain verdict enums; the host does the mapping. No reverse dependency — `pf-inject`/`pf-vdisplay` never learn about host types. - **`pf-vdisplay`**: `preflight_takeover_privilege()` now logs *from* `takeover_privilege_verdict()`. The WARN line is unchanged, deliberately: headless operators read logs, not consoles, so this moves where the verdict *goes*, not what it says. - **`pf-inject`**: `uinput_probe()` keeps the errno `pen_supported()` throws away, so "you are not in the `input` group" (EACCES) and "the module was never installed" (ENOENT) stop looking identical — they need opposite remedies. `vhci_probe()` reports device facts only; who to blame is the host's question, because only the user database can answer it. Two distinctions the catalog refuses to collapse: 1. **User-database membership vs this process's groups.** `usermod -aG` satisfies the first immediately and the second not until the next login. Collapsing them produces the most maddening support state there is — *"I already added myself!"* — which nothing in the logs distinguishes today. It gets its own remedy: log out, no command to run. 2. **`usermod` does not stick on an atomic OS.** On the Universal Blue images the remedy is `ujust add-user-to-input-group`, matched on the OS chain's **leaf**, never on the `fedora` family token — plain Fedora Workstation is mutable and does want `usermod`. ## Console The dashboard gets an `AttentionCard` that renders nothing at all on a healthy host (`ConflictsCard`'s rule), shows at most the 3 worst checks, and links onward rather than explaining. Its badge says the **severity**, not the status. The colour already encodes severity, so a badge reading "Failing" on both a red and an amber row leaves the difference carried by colour alone — the thing the `pin_pending` precedent exists to prevent. Caught on glass, not in the diff. The Logs page becomes Troubleshooting: checks above the log stream, because when the checks are green and something is still broken the log is the natural next step. The **route stays `/logs`** — bookmarks and deep links outlive a label. A check id this console has never heard of still renders, from the host's English `summary`/`impact`/`remedy.text`. That is what makes console N paired with host N+1 survivable, and it is enforced as a test rather than left as a convention. ## Reviewer notes — deliberate deviations from the plan - **The registry is a process-global `OnceLock`**, not an `AppState`/`MgmtState` field as §2 sketched. It matches `hooks::store()` / `detect::snapshot()`, lets the handlers take no `State` extractor, and leaves six call sites untouched. - **Timestamps are `u64` unix seconds** named `*_unix` (house style, cf. `started_unix`/`fetched_at`) rather than the design sketch's RFC-3339 — there is no date crate in the host. - **The console localizes check names + chrome only**, rendering the host's situational prose verbatim. One check has up to four failure shapes, so per-id `_summary`/`_impact`/`_remedy` keys would duplicate ~20 sentences into a package that versions independently — the exact drift §2.3 warns about. Full localization wants a `shape` discriminator on the wire first. Two traps the plan missed, both build-breaking and both handled here: `docs-site/public/openapi.json` is a second checked-in copy that nothing tests, and `every_route_is_classified_for_the_plugin_and_cert_lanes` fails until each new route gets an explicit lane row (diagnostics is `false, false` on both). ## Verification `cargo clippy -D warnings` and `cargo fmt --all --check` clean; 12 diagnostics unit tests and 48 mgmt handler tests green, including the openapi drift test — `api/openapi.json` and the ungated `docs-site` copy are both regenerated. Web: build, lint, `bun test server/`, storybook, and the new stories shot in both themes with the theme flip verified rather than assumed. **Not yet on glass**: the real `.181` box (all three vhci states), `.138` for the unpackaged-helper path, and the old-console/new-host pairing legs.
enricobuehler added 1 commit 2026-08-15 15:42:10 +00:00
feat(host,console): the host knew about every one of these faults and had no way to say so
ci / web (pull_request) Successful in 1m8s
ci / docs-site (pull_request) Successful in 1m18s
ci / rust (pull_request) Failing after 4m6s
ci / bun-nix (pull_request) Successful in 4m20s
android / android (pull_request) Successful in 13m55s
ci / rust-arm64 (pull_request) Successful in 6m14s
6776bff82d
Implements design/web-console-diagnostics.md, WP1-WP3 (the v1). WP4 (live checks + SSE)
and WP5 (instant helpers) stay deferred, as that plan sequences them.

The `.181` incident is the type specimen. `preflight_takeover_privilege()` is the most
careful probe in the repo — four applicability gates, and it even separates user-database
membership from the running process's supplementary groups — and it spends all of that
care on ONE WARN log line. A console-driven update never shows scriptlet stderr, so the
operator's only symptom was a black screen on every connect. The class of bug is "the host
knows, and the person operating it has no way to find out".

Host
----
`diagnostics.rs` holds the `HostCheck` model and a process-global registry (probes in,
cached verdicts out; `POST /refresh` re-runs them). `inapplicable` is a first-class status
rather than an absent row, so the page can answer "why isn't this check relevant here?"
instead of silently hiding it. `diagnostics/catalog.rs` maps verdicts to wire checks and
owns every user-visible string.

`GET /api/v1/diagnostics` + `POST /api/v1/diagnostics/refresh` are **admin lane only**.
Neither allowlist in `auth.rs` is touched: both are opt-in, and these verdicts carry the
host user's name, its group layout and device-node state. The route-classification test
gets both rows so that stays a reviewed decision rather than a default.

Probes stay in their owning crates and export plain verdict enums; the host does the
mapping. No reverse dependency — `pf-inject`/`pf-vdisplay` never learn about host types.

* `pf-vdisplay`: `preflight_takeover_privilege()` now logs FROM
  `takeover_privilege_verdict()`. The WARN line is unchanged, deliberately: headless
  operators read logs, not consoles, and this moves where the verdict GOES, not what it
  says.
* `pf-inject`: `uinput_probe()` keeps the errno that `pen_supported()` throws away, so
  "you are not in the input group" (EACCES) and "the module was never installed" (ENOENT)
  stop looking identical — they need opposite remedies. `vhci_probe()` reports device
  facts only (module present, node writable by this process); who to blame is the host's
  question, because only the user database can answer it.

Two distinctions the catalog refuses to collapse:

1. **User-database membership vs this process's groups.** `usermod -aG` satisfies the
   first immediately and the second not until the next login. Collapsing them produces
   the single most maddening support state there is — "I already added myself!" — which
   nothing in the logs distinguishes today. It now gets its own remedy: log out, no
   command to run.
2. **`usermod` does not stick on an atomic OS.** On the Universal Blue images the remedy
   is `ujust add-user-to-input-group`. Matched on the OS chain's LEAF, never on the
   `fedora` family token: plain Fedora Workstation is mutable and does want `usermod`.

Console
-------
The dashboard gets an `AttentionCard` that renders nothing at all on a healthy host
(`ConflictsCard`'s rule), shows at most the 3 worst checks, and links onward rather than
explaining — a dashboard that starts teaching remedies stops being a dashboard.

Its badge says the **severity**, not the status. The badge's colour already encodes
severity, so a badge reading "Failing" on both a red and an amber row leaves the
difference between them carried by colour alone — which is the thing the `pin_pending`
precedent exists to prevent. Caught on glass, not in the diff.

The Logs page becomes Troubleshooting: checks above the log stream, because when the
checks are green and something is still broken the log is the natural next step. The
ROUTE stays `/logs` — bookmarks and deep links outlive a label. `LogsCard` grows a
heading, since the page title no longer names it.

A check id this console has never heard of still renders, from the host's English
`summary`/`impact`/`remedy.text`. That is what makes console N paired with host N+1
survivable, and it is enforced as a test rather than left as a convention. The situational
prose is deliberately NOT duplicated into the message catalogues: one check has many
shapes (the vhci one alone has four distinct causes), and copying ~20 sentences into a
package that versions independently of the one that generates them is the very drift this
design set out to avoid. The console localizes the check NAMES and all chrome; when the
host sends a shape discriminator alongside `id`, localized prose can key off it.

Verification
------------
`cargo clippy -D warnings` and `cargo fmt --all --check` clean on Linux; 12 diagnostics
unit tests and 48 mgmt handler tests green, including the openapi drift test —
`api/openapi.json` and the ungated `docs-site/public/openapi.json` copy are both
regenerated. Web: build, lint, `bun test server/`, storybook, and the new stories shot in
both themes with the theme flip verified rather than assumed.

Not yet on glass: the real `.181` box (all three vhci states), `.138` for the
unpackaged-helper path, and the old-console/new-host pairing legs.
enricobuehler merged commit a7da3e4cb0 into main 2026-08-15 15:56:22 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#253