diff --git a/docs/plans/2026-08-11-dashboard-ui-sweeps.md b/docs/plans/2026-08-11-dashboard-ui-sweeps.md new file mode 100644 index 0000000..f8bbcd3 --- /dev/null +++ b/docs/plans/2026-08-11-dashboard-ui-sweeps.md @@ -0,0 +1,352 @@ +# Dashboard UI cleanup sweep (2026-08-11) + +Runs the family admin-UI cleanup playbook (`../scadaproj/admin_ui_cleanup.md`) against the +MXAccess Gateway Blazor dashboard. Third app in the family after the ignitionoee router and the +OtOpcUa AdminUI. + +## 0. Platform correction — this app is *not* Bootstrap-free + +The umbrella index describes mxaccessgw as the family's Bootstrap-free app. That is wrong, and +every Bootstrap-dependent recipe in the playbook applies here unchanged. What actually ships: + +| Layer | Evidence | +|---|---| +| Bootstrap 5.3.3, self-hosted | `libman.json:5`; `wwwroot/lib/bootstrap/css/bootstrap.min.css` | +| Linked first in the head | `Dashboard/Components/App.razor:7` | +| `ZB.MOM.WW.Theme` 0.3.1 at discovery, **0.4.0 after §6** | `ZB.MOM.WW.MxGateway.Server.csproj` ``; `` at `App.razor:8` | +| App stylesheet, loaded last | `App.razor:9` → `wwwroot/css/site.css` | +| Bootstrap JS bundle | `App.razor:14` | + +What CLAUDE.md actually forbids is Blazor **component libraries** (MudBlazor, Radzen, FluentUI) — +not Bootstrap's CSS/JS. No Bootstrap was introduced by this sweep. + +Playbook §2 foundation item 1 (*app stylesheet after `` so it wins the cascade*) was +therefore **already satisfied** before the sweep. + +## 1. Discovery + +### 1a. Foundation / CSS audit + +**Shipped-vs-used class matrix.** Every class in `Dashboard/Components/**/*.razor` checked against +theme 0.3.1 `staticwebassets/css/{theme,layout}.css`, `bootstrap.min.css`, and `site.css`. Three +classes are defined nowhere: + +| Ghost class | Site | Consequence | Verdict | +|---|---|---|---| +| `tree-load-status` | `Shared/BrowseTreeNodeView.razor:42`, `:49` | **Real visual defect.** The row's indent spacer is ``; `.tree-toggle` sets `flex:none;width:1.1rem`, which only takes effect on a flex item. `.tree-row`/`.tree-attr` are flex; this container is an undefined block, so the span stays inline and `width` is ignored — "⌛ Loading…" and "Failed to load: …" render flush left, out of alignment with every sibling row. | **Define it** | +| `browse-stale-banner` | `Pages/BrowsePage.razor:78` | The banner carries `@onclick="ClearStaleBanner"` with no pointer affordance and no styling of its own — a click-to-dismiss control that does not look clickable. | **Define it** | +| `tree-node` | `Shared/BrowseTreeNodeView.razor:15` | Structural wrapper only; nothing needs to style it. A no-op, but a deliberate one. | **Leave** | + +(Reported as ghosts by the raw extractor but false positives: `h-100` at `Shared/MetricCard.razor:1` +— Bootstrap-defined, mangled by the extractor's handling of the inline `@(...)` class expression.) + +**Scoped-CSS bundle.** N/A — the project contains **zero** `*.razor.css` files, so no +`*.bundle.scp.css` is emitted and nothing is missing from the head. (This was OtOpcUa's finding; it +does not exist here.) + +**Phantom CSS custom properties.** Zero. `Dashboard/Components/` contains no `var(--…)` at all — +tokens are used only from `site.css`, and every one of them (`--ink`, `--ink-soft`, `--ink-faint`, +`--card`, `--rule`, `--rule-strong`, `--mono`, `--accent`, `--accent-deep`, `--ok`, `--ok-bg`, +`--bad`, `--bad-bg`, `--warn`, `--warn-bg`, `--idle`, `--idle-bg`) resolves against theme 0.3.1. +This app has the OtOpcUa-clean result, not the router's. + +**Button sizing — the one real foundation defect.** Theme 0.3.1 ships no `.btn` rule (confirmed: +`.btn` appears in `layout.css` only inside a comment). But `site.css:187` does, and it sets +`font-size` **directly** rather than through Bootstrap's variable: + +```css +.btn { border-radius: 5px; font-size: 0.82rem; font-weight: 500; white-space: nowrap; } +``` + +Bootstrap renders `.btn { font-size: var(--bs-btn-font-size) }`, and `.btn-sm` / +`.btn-group-sm > .btn` size themselves purely by *redefining that variable* +(`.btn-sm{--bs-btn-font-size:0.875rem}`). A literal `font-size` on `.btn` at equal specificity, +loaded later, wins over the variable-driven declaration for **every** button — so `btn-sm` and +`btn-group-sm` are font-size no-ops app-wide and small buttons differ from full-size ones by +padding alone. This is the same class of defect the other two apps hit from the opposite +direction (no `.btn` rule at all), and it takes the same fix. + +**Dark scheme.** Theme 0.3.1 is light-only; `site.css` makes no `prefers-color-scheme` / +`data-bs-theme` claim, and its header comment ("Layers over theme.css … every colour resolves to a +theme.css token") is accurate. Nothing to correct. + +### 1b. Button inventory + +`grep -rn "btn-group"` returns **three** — this app already uses the convention where it matters: + +| Site | Members | State | +|---|---|---| +| `Pages/ApiKeysPage.razor:189` | Rotate / Revoke, or Delete | Correct — `btn-group btn-group-sm`, no per-button `btn-sm`, `@if` inside the group | +| `Pages/SessionsPage.razor:90` | Close / Kill | Correct | +| `Pages/SessionDetailsPage.razor:34` | Close session / Kill worker | Correct | + +Adjacent related buttons **not** yet grouped (both are feet, the playbook's named case): + +| Site | Members | Fix | +|---|---|---| +| `Shared/ConfirmDialog.razor:17,22` | Cancel + `@ConfirmButtonClass` confirm, in a `modal-footer` | Wrap in `btn-group` | +| `Pages/ApiKeysPage.razor:136,137` | Save (`type="submit"`) + Cancel, in the create-key card body | Wrap in `btn-group btn-group-sm`; fold the two `btn-sm` and drop the `me-1` spacer | + +Refused / not candidates: + +- `Pages/WorkersPage.razor:74` — a lone Kill button. Nothing to group. +- `Pages/ApiKeysPage.razor:22` — lone page-head "Create API Key". +- `Pages/BrowsePage.razor:109` (`Clear all`) and `:167` (`Remove`) — lone buttons. +- `Shared/BrowseTreeNodeView.razor:19` `.tree-toggle` — a bare expander, deliberately unstyled as a + button; `MainLayout.razor:32` Sign Out / `:36` Sign In are the theme's `rail-btn`, one per + auth branch and mutually exclusive. + +**Row actions styled as links**: none. Every action in the app is already a real `