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 fromtakeover_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:
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
.181incident 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.rsholds theHostCheckmodel and a registry (probes in, cached verdicts out;POST /refreshre-runs them).inapplicableis 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.rsmaps verdicts to wire checks and owns every user-visible string.GET /api/v1/diagnostics+POST /api/v1/diagnostics/refreshare admin lane only. Neither allowlist inauth.rsis 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-vdisplaynever learn about host types.pf-vdisplay:preflight_takeover_privilege()now logs fromtakeover_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 errnopen_supported()throws away, so "you are not in theinputgroup" (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:
usermod -aGsatisfies 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.usermoddoes not stick on an atomic OS. On the Universal Blue images the remedy isujust add-user-to-input-group, matched on the OS chain's leaf, never on thefedorafamily token — plain Fedora Workstation is mutable and does wantusermod.Console
The dashboard gets an
AttentionCardthat 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_pendingprecedent 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
OnceLock, not anAppState/MgmtStatefield as §2 sketched. It matcheshooks::store()/detect::snapshot(), lets the handlers take noStateextractor, and leaves six call sites untouched.u64unix 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._summary/_impact/_remedykeys would duplicate ~20 sentences into a package that versions independently — the exact drift §2.3 warns about. Full localization wants ashapediscriminator on the wire first.Two traps the plan missed, both build-breaking and both handled here:
docs-site/public/openapi.jsonis a second checked-in copy that nothing tests, andevery_route_is_classified_for_the_plugin_and_cert_lanesfails until each new route gets an explicit lane row (diagnostics isfalse, falseon both).Verification
cargo clippy -D warningsandcargo fmt --all --checkclean; 12 diagnostics unit tests and 48 mgmt handler tests green, including the openapi drift test —api/openapi.jsonand the ungateddocs-sitecopy 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
.181box (all three vhci states),.138for the unpackaged-helper path, and the old-console/new-host pairing legs.