# Web Console: restart status, impact preview, and approval state (#667) Phase 1 of the console restart surface. It consumes the #655 coordinator substrate and displays it. It performs no restart, reload, drain, approval, or process action, and it registers no write endpoint. Issue #667's rollout is explicit — *status views first, write approval after the backend gates are green* — and this change delivers only the status half. ## Surfaces | Path | Method | Purpose | |------|--------|---------| | `/runtime/restart` | GET | Restart status page | | `/api/v1/system/restart/status` | GET | Same snapshot as JSON | Both accept an optional `restart_class` query parameter (default `full_mcp_restart`). An unrecognised class is not an error: the coordinator resolves it as unknown and fails closed, and the page shows the resulting deny. Neither path accepts `POST`; a write attempt returns `405`, and a test asserts it. ## What it shows * **Impact preview (#658)** — verdict, blast radius, affected sessions, leases, critical sections, mutations, and the counts behind them, evaluated `dry_run=True` against live control-plane state. * **Drain proof (#661)** — verification of a supplied proof: valid, clean, expired, tampered, and the reasons behind a refusal. * **Post-restart reconcile (#662)** — the most recent completion proof, its overall status, and which dimensions still require follow-up. * **Restart classes (#663)** — the least-privilege matrix, with *you may request* and *you may execute* computed for the viewing role rather than for a generic operator. * **Approval controls (#633)** — the authorization state of `system.restart_namespace` and `system.reload_namespace`. * **Break-glass (#664)** — declared and marked unavailable; see below. ## Three rules this surface holds itself to A status page that is wrong is worse than one that is missing, because an operator acts on it. Three properties are enforced by tests, and each was verified by reverting the guard and watching a test fail. ### An unreadable source reports unavailable, never green Every source carries its own `SourceStatus`. Nothing substitutes a default, placeholder, or self-comparison for a reading that failed. An unreadable control-plane database yields `inventory_complete: false`, which the coordinator itself turns into a fail-closed verdict, and the page says the blast radius is unknown rather than showing an empty affected-sessions table. An absent drain proof is reported as absent — not as a pass. The #661 gate authorizes a restart only against a valid, unexpired, clean proof, so no proof is precisely the state that gate denies on. ### Authorization is asked the way execution would ask it Every probe passes `for_execution=True`. Asked without it, an admin is `allowed` for `system.restart_namespace`. On a control surface that reads as a live button. Asked the way an execution attempt would ask, the same principal is refused `phase_not_active`, because the console is in Phase 1 and the action is Phase 2. This surface reports the second answer. `execution_enabled` is therefore `false` for every action and every role today, and a test asserts that across the whole role matrix. ### The control-plane database is opened read-only `ControlPlaneDB()` creates directories and runs migrations on construction — a write. This surface never constructs one. It opens the sqlite file with `mode=ro`, exactly as `webui/inventory.py` does, and treats a missing file as missing authority rather than as an empty inventory. The test that protects this points at a path inside a directory that already exists, so a read-write `connect` would really create the file. A nested missing-directory path would have passed for the wrong reason. ## Break-glass is declared, not offered The break-glass workflow (#664) is not available on this branch's base. The panel is rendered to operator-class roles as **unavailable**, naming the issue that tracks it. It is not silently omitted, because an operator who has been told a governance path exists needs to see that it is not wired here; and it is not rendered as a control, because there is nothing behind it. Unprivileged viewers see only a note that the surface is operator-class. ## Redaction and escaping Every interpolated value passes through `_esc` (`html.escape(..., quote=True)`). Free-form text and anything that can carry a filesystem path additionally passes through `webui.inventory.scrub_text`, which redacts credential-shaped tokens inside a string rather than only at its start. The impact payload is passed through `webui.inventory.scrub` before rendering. ## Linkage Parent #655 · extends #642 · consumes #658, #661, #662, #663 · RBAC #633 · console #631 · vision #652 · roadmap #653 · break-glass #664.