01033d7aaf
Family-wide admin-UI cleanup pass (scadaproj admin_ui_cleanup.md) applied to the Blazor dashboard. Behaviour is unchanged throughout — no @onclick, disabled, binding, auth gate, or arm->confirm flow was touched. Uncontrolled error text is now truncated at the render site. Fault messages, Galaxy load errors, and browse-tree load failures were rendered in full into fixed-width table cells, where a long exception string blows out the column. Each site gets DashboardDisplay.Abbreviate plus a title attribute carrying the untruncated text, so nothing becomes unreachable. Abbreviate is length-checked rather than a bare range slice: `value[..n]` on a shorter string throws and takes the whole page render down with it. The two detail views whose entire purpose is to show one fault in full — SessionDetailsPage and GalaxyPage's Last Error — are deliberately left untruncated. Two classes referenced from markup had no definition anywhere in the sheet. .browse-stale-banner was inert; .tree-load-status was a real visual defect — loading and failed-to-load rows sit among .tree-row siblings and carry the same leading .tree-toggle-empty spacer, but that spacer only takes its width as a flex item, so without a flex container those rows lost their indent. Confirm/cancel pairs in ConfirmDialog and the API-key create form are now btn-groups with role="group" and an aria-label, replacing margin-spaced loose buttons. Removes a paragraph on GalaxyPage naming internal RPCs (DiscoverHierarchy, GetLastDeployTime) — implementation detail with no meaning to a dashboard operator. Verified in a real browser, not bUnit: full build clean, 879/879 tests, and a live gate against a running dashboard with a genuine ~250-char SqlClient exception as the erroring row. Results per check, including the checks that could NOT be exercised without an x86 worker, are recorded in docs/plans/2026-08-11-dashboard-ui-sweeps.md. That plan doc also records a correction: this app is NOT Bootstrap-free. The sweep brief said it was, citing the scadaproj index; libman.json pins bootstrap 5.3.3 and App.razor:7 links it ahead of the theme. The stale claim had already cost this app one skipped family sweep (scadaproj#2, the /admin/secrets modal), so that modal was live-gated here too and passes.
353 lines
24 KiB
Markdown
353 lines
24 KiB
Markdown
# 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` `<PackageReference>`; `<ThemeHead />` 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 `<ThemeHead />` 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 `<span class="tree-toggle tree-toggle-empty">`; `.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 `<button>`; the
|
||
only `<a>` in `Dashboard/Components/` is `MainLayout.razor:36`, which navigates. `btn-link`: zero
|
||
uses. `·` action separators: zero — the `·` occurrences (`BrowsePage.razor:54,102,106,287`,
|
||
`BrowseTreeNodeView.razor:71`, `AlarmsPage.razor:220`) all separate *metadata facts* or serve as a
|
||
bullet glyph, never actions. Inline `width:` sizing hacks: zero. `py-0`: zero.
|
||
|
||
**Arm→confirm flows** (restyle-only; the two-step must survive):
|
||
|
||
| Page | Flow |
|
||
|---|---|
|
||
| `SessionsPage` | Close / Kill → `ConfirmDialog` → `ConfirmPendingAsync` |
|
||
| `SessionDetailsPage` | Close session / Kill worker → `ConfirmDialog` |
|
||
| `WorkersPage` | Kill → `ConfirmDialog` |
|
||
| `ApiKeysPage` | Rotate / Revoke / Delete → `ConfirmDialog`; Create → modal form |
|
||
|
||
**Size mismatches within a group**: none.
|
||
|
||
### 1c. Prose inventory (DELETE / KEEP / RELOCATE)
|
||
|
||
| Site | Text | Verdict |
|
||
|---|---|---|
|
||
| `Pages/GalaxyPage.razor:134-138` | "Browse data is served by the `galaxy_repository.v1.GalaxyRepository` gRPC service. Clients call `DiscoverHierarchy` for the full tree and `GetLastDeployTime` to detect redeployments." | **DELETE** — unconditional client-API documentation on an operator page. Every fact is already in `docs/GalaxyRepository.md` (service name at :44, `GetLastDeployTime` at :49, `DiscoverHierarchy` at :50). Plain delete, no relocation needed. |
|
||
| `Pages/AlarmsPage.razor:151-154` | "Cleared alarms are not retained — this list reflects only alarms currently Active or ActiveAcked, refreshed every 3 seconds." | KEEP — decodes what the list contains and does not contain; the operator cannot infer "cleared alarms are absent" from the data. |
|
||
| `Pages/AlarmsPage.razor:26-30` | Alarms-disabled banner citing `MxGateway:Alarms:Enabled` | KEEP — conditional state banner. **Config key verified against the options class**: `GatewayOptions.Alarms` → `AlarmsOptions.Enabled`, present in `appsettings.json`. |
|
||
| `Pages/BrowsePage.razor:33-35`, `:41`, `:120-124` | Empty states | KEEP |
|
||
| `Pages/BrowsePage.razor:70`, `:90` | "Showing the first N matches — refine the filter." / "Double-click a tag, or right-click for the menu." | KEEP — truncation notice, and the only decoder of two interactions that have no visible affordance. |
|
||
| `Pages/SessionDetailsPage.razor:115-118` | "Waiting for events. The dashboard mirrors the session's gRPC event stream — events appear here only while a gRPC client is also consuming this session's events." | KEEP — explains an empty state that otherwise reads as a bug. |
|
||
| `Pages/GalaxyPage.razor:33-38` | Unknown-status empty state | KEEP |
|
||
|
||
**Stale-claim hunt** (wrong facts outrank style): none found. No milestone labels ("F8/F9 pending",
|
||
"Batch 2"), no dead repo hyperlinks, no superseded architecture claims. Every config key cited in
|
||
markup resolves — `MxGateway:Alarms:Enabled` (above) is the only one. Also checked and **cleared**:
|
||
`SettingsPage.razor:32` renders `Ldap.ServiceAccountPassword`, but
|
||
`Configuration/GatewayConfigurationProvider.cs:30` substitutes `RedactedValue` before the snapshot
|
||
is built, so no credential reaches the page.
|
||
|
||
### 1d. Density / layout scan (per page, not per file)
|
||
|
||
Structural mitigations already present app-wide: `site.css:137` caps `.dashboard-table td` at
|
||
`max-width: 26rem` with `overflow-wrap: break-word`, so the OtOpcUa `/hosts` failure mode (one long
|
||
exception widens the table until the actions column scrolls off) **cannot happen here**. Every table
|
||
is also inside `.table-responsive`. `panel-head` appears zero times, so the "sections running
|
||
together" tell does not fire — each group is its own `section.dashboard-section` card already.
|
||
|
||
Residual finding — **unbounded free-text columns**. The 26rem cap converts the horizontal blowout
|
||
into vertical blowout: a multi-line exception makes one row several times taller than its
|
||
neighbours and pushes the rest of the table off-screen.
|
||
|
||
| Site | Column | Bound to |
|
||
|---|---|---|
|
||
| `Shared/FaultList.razor:28` | Message | `@fault.Message` — worker/COM fault text, uncontrolled |
|
||
| `Pages/SessionsPage.razor:86` | Fault | `session.LastFault` |
|
||
| `Pages/WorkersPage.razor:70` | Fault | `worker.LastFault` |
|
||
| `Shared/BrowseTreeNodeView.razor:51` | (tree row) | `@Node.LoadError` — **worst case**: `.tree-attr`/`.tree-row` siblings are `white-space: nowrap` inside a fixed-height scroller, so a long browse error stretches the left pane horizontally |
|
||
| `Pages/DashboardHome.razor:47` | Galaxy panel | `Snapshot.Galaxy.LastError` on the compact overview |
|
||
|
||
Deliberately **not** truncated (the full text must stay reachable — playbook rule):
|
||
`SessionDetailsPage.razor:84` "Last fault" and `GalaxyPage.razor:47` "Last Error" are both
|
||
full-width rows on the drill-down page each summary links to.
|
||
|
||
Not firing: **identity slam** — `SessionsPage.razor:75-81` puts a worker pid next to a status chip,
|
||
which the playbook explicitly permits ("chips stay with the name line"); no cell renders two
|
||
identifiers. **Inline full-width expander rows** — zero `colspan` detail rows in the app.
|
||
|
||
### 1e. Tree tables
|
||
|
||
**No adoption.** `BrowsePage` + `BrowseTreeNodeView` is a lazy-loading nav/picker tree with a
|
||
context menu and no columns — the rubric's explicit "different animal, leave it alone". No table in
|
||
the app flattens hierarchy through a path-prefix column, hand-rolled `rowspan`, indent-by-padding,
|
||
or faked group-header rows; `Sessions`/`Workers`/`Events`/`Alarms`/`Galaxy` are flat fact lists and
|
||
`Top Templates` is rank-ordered (a tree would destroy load-bearing ordering). Porting the router's
|
||
`TreeTable` here would create an orphan.
|
||
|
||
### Guard tests
|
||
|
||
Checked for source-scan tests pinning dashboard markup: none. The only `.razor` reference in the
|
||
test project is a prose comment (`Gateway/GatewayApplicationTests.cs:116`). Nothing to re-pin.
|
||
|
||
## 2. Changes
|
||
|
||
### Foundation
|
||
|
||
1. **`wwwroot/css/site.css` — button sizing via Bootstrap CSS variables.** Replace the literal
|
||
`font-size` on `.btn` with `--bs-btn-*` overrides and add the `btn-sm` / `btn-group-sm` block, so
|
||
the small-button distinction works again. Box properties are *not* redefined wholesale —
|
||
`border-radius: 5px` stays literal because the three existing `btn-group`s already render seamed
|
||
under it (`.btn-group > .btn:not(:first-child)` outranks `.btn`). Carries the header comment the
|
||
other two apps carry: delete the local copy when a `ZB.MOM.WW.Theme` release ships a `.btn` rule.
|
||
**That release shipped the same day — see §6.**
|
||
2. **`site.css` — define `.tree-load-status`** as a flex row matching `.tree-row`, restoring the
|
||
indent alignment of the tree's loading/error rows.
|
||
3. **`site.css` — define `.browse-stale-banner`** with `cursor: pointer` and tightened padding, so
|
||
the click-to-dismiss banner reads as clickable. No behavior change.
|
||
4. **`Dashboard/Components/DashboardDisplay.cs` — add `Abbreviate(string?, int)`.** Safe helper
|
||
(length-checked, never a raw `[..n]` slice — the OtOpcUa #504 failure), `-` for null/whitespace,
|
||
ellipsis on truncation.
|
||
|
||
No prose relocation task: the single DELETE is already docs-covered.
|
||
|
||
### Page batches
|
||
|
||
**Batch A — free-text truncation** (`Shared/FaultList.razor`, `Pages/SessionsPage.razor`,
|
||
`Pages/WorkersPage.razor`, `Shared/BrowseTreeNodeView.razor`, `Pages/DashboardHome.razor`):
|
||
`Abbreviate` + full text on `title`, at the five sites in 1d.
|
||
|
||
**Batch B — button feet** (`Shared/ConfirmDialog.razor`, `Pages/ApiKeysPage.razor`): the two
|
||
`btn-group` wraps from 1b. `@onclick`, `disabled`, `type="submit"`, and both arm→confirm flows
|
||
unchanged.
|
||
|
||
**Batch C — prose** (`Pages/GalaxyPage.razor`): delete `:134-138`.
|
||
|
||
## 3. Verification
|
||
|
||
### Build + tests
|
||
|
||
- [x] `dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx` — 0 warnings, 0 errors
|
||
(`TreatWarningsAsErrors=true`).
|
||
- [x] `dotnet test src/ZB.MOM.WW.MxGateway.Tests` — 879/879 passed, the macOS baseline.
|
||
|
||
### Post-merge greps
|
||
|
||
- [x] Zero phantom `var(--…)` usages (there were none to begin with).
|
||
- [x] Zero ghost classes remaining except the deliberate `tree-node`.
|
||
- [x] `btn-group` count 3 → 5, matching the 1b inventory.
|
||
- [x] Zero `·` action separators, zero inline `width:` hacks (none existed).
|
||
- [x] Scoped-bundle link: N/A, no `*.razor.css`.
|
||
|
||
### Live browser gate
|
||
|
||
Recorded in §4 below.
|
||
|
||
## 4. Live browser gate — results
|
||
|
||
Run against a local `dotnet run` of the gateway on macOS (`http://localhost:5120`, the launch
|
||
profile's port), not windev — the sweep is CSS/markup-only and redeploying the shared NSSM service
|
||
was not warranted. Rig configuration (env overrides only, no committed config touched):
|
||
`Dashboard:DisableLogin=true` (to reach the Admin-only surfaces), `Ldap:Enabled=false`,
|
||
`Authentication:Mode=Disabled`, SQLite + Galaxy snapshot paths under the session scratchpad,
|
||
`ApiKeyPepper` set locally. The rig was stopped and its throwaway auth DB deleted afterwards.
|
||
|
||
**Realistic erroring data**: the Galaxy SQL Server (`localhost`, `ZB`) does not exist on macOS, so
|
||
every refresh fails with a genuine 250-character `Microsoft.Data.SqlClient` SSPI exception, and the
|
||
alarm monitor's auto-opened session fails on the missing x86 worker — both real, uncontrolled
|
||
error text of exactly the kind §1d is about.
|
||
|
||
| # | Check | Result | Evidence |
|
||
|---|---|---|---|
|
||
| 1 | Computed-style probes | **PASS** | body `14.4px`; `.btn` `13.6px` (0.85rem); `.btn-sm` `12.48px` (0.78rem) — the small/full distinction is live, where before the fix both computed to `13.12px`. `btn-group-sm` members: both `12.48px`, first member's right radius `0px`, last member's `5px` → **seams intact**, confirming the literal `border-radius` does not break group machinery. `.tree-load-status` → `display: flex`; `.browse-stale-banner` → `cursor: pointer`. Stylesheet order: bootstrap → theme → layout → **site.css last**. The theme's own `.rail-toggle btn-sm` also picks up `12.48px`, so the block reaches theme-owned buttons, not just app markup. |
|
||
| 2 | Every page leads with data, no unconditional doc block above the fold | **PASS** | All 9 routes' first blocks are `dashboard-page-header` → `metric-grid` / `dashboard-section` / state `alert`. `/galaxy` now ends at the Sync Info table — the deleted client-API paragraph is gone. |
|
||
| 3 | Seamed `btn-group`s; destructive members red | **PASS** | `/apikeys` row actions render Rotate + Revoke as one seamed group with Revoke in `btn-outline-danger` red. The `ConfirmDialog` foot renders Cancel + a solid red Revoke seamed together. |
|
||
| 4 | Arm→confirm: arm → confirm UI appears → **Cancel** → verify disarm | **PASS** | Revoke → "Revoke API key?" dialog → Cancel. Post-cancel probe: `dialogOpen:false`, `backdrops:0`, key status still `Active`, row action back to `btn-outline-danger` outline. No write completed. Save/Cancel in the create-key modal also verified: Cancel closes without creating; Save creates (validation message surfaced correctly on the first attempt with a missing display name). |
|
||
| 5 | Anything the scoped bundle resurrected | **N/A** | No `*.razor.css` in the project — see 1a. |
|
||
| 6 | KEEP-list legends / hints / empty states still render | **PASS** | `/browse`: both empty states plus the "Right-click a tag … Add to subscription panel" hint. `/alarms`: the "Cleared alarms are not retained …" legend under the table. `/galaxy`: Object Categories / Top Templates empty states. |
|
||
| 7 | Density with realistic data; sections as separate cards; no horizontal scroll | **PASS** | `/` renders the SQL error truncated to one line with `…`; `/galaxy` renders the same error in full (the drill-down) — the intended split is visible side by side. `/apikeys` with a 3-entry `read_subtrees` constraint wraps inside the column with no table overflow. `documentElement.scrollWidth == clientWidth` on every page walked. Each group is its own card. |
|
||
|
||
**Not exercisable on this rig** (recorded, not claimed as passing): the `Sessions` / `Workers` /
|
||
`Recent Faults` truncation call sites and the `BrowseTreeNodeView` load-error row need a *registered*
|
||
session, which requires the x86 worker — on macOS the launch fails before the session is ever
|
||
registered, so those tables stay empty. Their shared helper and the container CSS were both verified
|
||
live by other means (the `/` Galaxy error for `Abbreviate`, the computed-style probe for
|
||
`.tree-load-status`). Re-check them on windev the next time that service is redeployed.
|
||
|
||
**Side note surfaced, not acted on**: `SettingsPage` renders `LDAP service password` as
|
||
`[redacted]` in the browser — confirming `GatewayConfigurationProvider`'s substitution end to end.
|
||
|
||
## 5. Follow-up gate: `/admin/secrets` delete modal (scadaproj#2)
|
||
|
||
Not part of the sweep. Run because the same stale "mxgw is Bootstrap-free" claim corrected in §0 was
|
||
the stated reason this app was **excused** from the 2026-07-19 family-wide `/admin/secrets` modal
|
||
sweep: `ConfirmDeleteModal` shipped a bare `class="modal"`, which Bootstrap 5's `.modal{display:none}`
|
||
made permanently invisible on every Bootstrap host. The exemption's reasoning was false, so mxgw was
|
||
in scope and its delete modal had never been live-gated. `8f7ee49` bumped `ZB.MOM.WW.Secrets.Ui` to
|
||
`0.2.3`, which is supposed to carry the fix — but "supposed to" is what the original claim was.
|
||
|
||
**Static check** — decoding the UTF-16 literals out of `ZB.MOM.WW.Secrets.Ui.dll` 0.2.3 shows the
|
||
component now inlines its own `<style>` under a private namespace: `.zb-secrets-modal` with
|
||
`display: flex; position: fixed; inset: 0; z-index: 1081`, over a `.zb-secrets-modal-backdrop` at
|
||
`z-index: 1080`. No bare `modal` class.
|
||
|
||
**Live check** — local rig, scratch secrets store (`Secrets:SqlitePath` + `ZB_SECRETS_MASTER_KEY`
|
||
under the session scratchpad), throwaway secret `uisweep/throwaway`, delete armed but **never
|
||
confirmed**:
|
||
|
||
| Probe | Result |
|
||
|---|---|
|
||
| Modal root classes | `zb-secrets-modal` — and `document.querySelector('.modal')` is `null`, so Bootstrap's `.modal{display:none}` has nothing to bite |
|
||
| Modal computed style | `display: flex`, `position: fixed`, `z-index: 1081`, `visibility: visible`, `opacity: 1`, box `1600×827` |
|
||
| Backdrop | `display: block`, `position: fixed`, `z-index: 1080`, `rgba(15,18,20,.45)`, covers the full viewport |
|
||
| Confirm button hit-testable | `elementFromPoint` at its centre returns the button itself — not covered by the backdrop |
|
||
| Cancel → disarm | modal and backdrop both removed; secret still listed; **no delete performed** |
|
||
|
||
**PASS.** The `0.2.3` fix holds on this Bootstrap host. The rig was stopped and both throwaway
|
||
stores (secrets + auth) deleted.
|
||
</content>
|
||
|
||
## 6. Follow-up: `ZB.MOM.WW.Theme` 0.3.1 → 0.4.0 (dependency bump, not UI cleanup)
|
||
|
||
0.4.0 upstreams the button-sizing block this sweep installed locally, written against the finding in
|
||
§1a — the kit sizes **only** through `--bs-btn-*` variables and carries a comment in `layout.css`
|
||
saying why, so nobody simplifies it back into literals.
|
||
|
||
- `ZB.MOM.WW.MxGateway.Server.csproj`: `0.3.1` → `0.4.0`. No `Directory.Packages.props` in this repo;
|
||
the version lives on the `PackageReference`.
|
||
- `site.css`: the four `--bs-btn-*` overrides and the whole `.btn-group-sm > .btn, .btn-sm` rule
|
||
deleted — `layout.css` now supplies them and `<ThemeHead />` emits it before `site.css`.
|
||
|
||
**Trimmed, not deleted wholesale.** The local `.btn` rule also carried `border-radius: 5px`,
|
||
`font-weight: 500`, `white-space: nowrap`, which predate the sweep and which 0.4.0 does **not**
|
||
ship — it upstreamed sizing only. Removing the block entirely would have silently dropped three
|
||
app-specific declarations. What remains is `.btn { border-radius: 5px; font-weight: 500;
|
||
white-space: nowrap; }` — shape, not size. `border-radius` deliberately stays a literal rather than
|
||
`--bs-btn-border-radius`, because `.btn-sm` redefines that variable and small buttons would shrink
|
||
to the Bootstrap small radius.
|
||
|
||
**Verification**
|
||
|
||
- Build: 0 warnings, 0 errors. Suite: 879/879.
|
||
- Computed-style probe re-run on 0.4.0 with the local block gone: `.btn` **13.6px**, `.btn-sm`
|
||
**12.48px** — identical to the pre-bump numbers, so the kit rule reaches. `btn-group-sm` members
|
||
both 12.48px with the seam intact (first member right radius `0px`, last `5px`). Retained
|
||
declarations confirmed live on both sizes: radius `5px`, weight `500`, `nowrap`.
|
||
- Cascade confirmed by enumerating `document.styleSheets`: `--bs-btn-font-size` is now declared in
|
||
exactly two sheets — `bootstrap.min.css` (`1rem` / `0.875rem`) and `layout.css`
|
||
(`.85rem` / `.78rem`). `site.css` no longer declares it, so the duplicate is gone.
|
||
|
||
**Tree-hygiene note.** The first suite run after the bump failed
|
||
`GatewayTreeHygieneTests.SourceTree_ContainsNoSqliteDatabaseFiles` — unrelated to the bump. The §4
|
||
rig's *first* start used the default **relative** `Secrets:SqlitePath` (`mxgateway-secrets.db`),
|
||
which resolved against the server project directory and left a DB in the source tree; only the §5
|
||
run redirected it to the scratchpad. The file was untracked, was deleted, and the suite went green.
|
||
The §4/§5 cleanup notes were therefore incomplete as originally written — the scratchpad copies were
|
||
removed but this one was missed. The hygiene test is what caught it, which is what it exists for.
|
||
|
||
## 7. Follow-up: `ZB.MOM.WW.Theme` 0.4.0 → 0.4.1 (pin-only, no rendering change here)
|
||
|
||
0.4.1 was published the same day to fix `.rail-btn-block`, a modifier shipped broken in 0.4.0
|
||
(`display: block; width: auto` fills for an `<a>` but shrink-wraps a `<button>`; now
|
||
`width: calc(100% - 1.2rem)` with `box-sizing: border-box`, accounting for `.rail-btn`'s side
|
||
margins). Bumped for family-pin alignment.
|
||
|
||
**Why nothing needed re-verifying.** Confirmed rather than assumed:
|
||
|
||
- `diff` of the two restored packages: `theme.css` byte-identical; `layout.css` differs **only**
|
||
inside the `.rail-btn-block` rule and its comment. The `.btn` sizing block is unchanged, so the
|
||
§6 measurements (`.btn` 13.6px, `.btn-sm` 12.48px) still hold and the probe was not re-run.
|
||
- This app does not use `rail-btn-block`. It does carry the exact element pair the bug turned on —
|
||
`MainLayout.razor:32` is a form-submit `<button class="rail-btn">` (Sign Out) and
|
||
`MainLayout.razor:36` an `<a class="rail-btn">` (Sign In) — but base `.rail-btn` is
|
||
`display: inline-block`, which shrink-wraps both element types identically. The asymmetry only
|
||
appears once the block modifier is applied, so this dashboard was never affected.
|
||
|
||
**Verification.** Build 0 warnings / 0 errors; suite **879/879**; `staticwebassets.build.json`
|
||
resolves `zb.mom.ww.theme/0.4.1`. No stale-HTTP-cache clear was needed — restore picked 0.4.1
|
||
directly.
|