Secrets.Ui delete/confirm modal is invisible on every Bootstrap host — bare .modal class collides with Bootstrap's display:none, and the RCL ships no CSS #2

Closed
opened 2026-07-19 00:58:22 -04:00 by dohertj2 · 2 comments
Owner

Found 2026-07-19 while live-verifying the ScadaBridge#22 fix (the row Delete button appeared dead). Distinct from both lmxopcua#483 (render mode) and ScadaBridge#22 (missing audit seam) — this one is in the Secrets.Ui library itself.

Symptom

Clicking a secret row's Delete button does nothing visible. The circuit is alive and ShowDelete runs — [data-testid="delete-modal"] attaches to the DOM — but the modal never appears:

delete-modal attached; visible=false; computed display=none

Reproduced on ScadaBridge Central (localhost:9001) and OtOpcUa AdminUI (localhost:9200). HistorianGateway links the same Bootstrap bundle and is expected identical. MxGateway ships no Bootstrap and is the only host where the bare markup can render.

Cause

ConfirmDeleteModal.razor renders <div class="modal" ...> and ZB.MOM.WW.Secrets.Ui ships no CSS at all (no wwwroot css, no scoped .razor.css). The component silently depends on the host having no opinion about .modal — but Bootstrap 5 (linked by OtOpcUa, HistorianGateway, and ScadaBridge's Host wwwroot) defines .modal { display: none; } and only shows it when its own JS adds .show. The RCL's modal is pure Blazor (@if (Visible)) and never gets Bootstrap's class, so it renders permanently hidden. modal-backdrop-inpage / modal-card / btn-row are similarly unstyled.

The defect was invisible to every prior check: the family sweep's Playwright gate only exercised Add secret (the in-page SecretEditor, which uses no modal), and the modal's @onclick handlers are fully functional — forcing el.style.display='block' makes the whole delete flow work (that is how the probe secrets were cleaned up).

Suggested fix (in ZB.MOM.WW.Secrets.Ui)

  1. Rename the classes to a component-scoped vocabulary (zb-secrets-modal, zb-secrets-modal-backdrop, zb-secrets-modal-card) so no host framework can collide.
  2. Ship the styles with the RCL — scoped ConfirmDeleteModal.razor.css (bundles into the consumer's *.styles.css automatically) covering overlay positioning, backdrop, and card, consistent with the Theme tokens.
  3. Add a Playwright/bUnit check that the modal is actually visible after clicking Delete (computed display != none), on a host page that links Bootstrap — the collision only reproduces there.

Verification recipe

On any Bootstrap host (e.g. OtOpcUa docker-dev :9200): create a secret, click its row Delete, assert [data-testid="delete-modal"] is visible and [data-testid="confirm-delete"] is clickable without JS style forcing. Rig states: both rig probe secrets (tmp/modal-probe on :9200, sb22/audit-bridge-proof on :9001) were force-deleted through the real ConfirmAsync path, so stores are clean.

Found 2026-07-19 while live-verifying the ScadaBridge#22 fix (the row **Delete** button appeared dead). Distinct from both `lmxopcua#483` (render mode) and `ScadaBridge#22` (missing audit seam) — this one is in the **Secrets.Ui library itself**. ## Symptom Clicking a secret row's **Delete** button does nothing visible. The circuit is alive and `ShowDelete` runs — `[data-testid="delete-modal"]` attaches to the DOM — but the modal never appears: ``` delete-modal attached; visible=false; computed display=none ``` Reproduced on **ScadaBridge Central** (localhost:9001) and **OtOpcUa AdminUI** (localhost:9200). HistorianGateway links the same Bootstrap bundle and is expected identical. MxGateway ships no Bootstrap and is the only host where the bare markup can render. ## Cause `ConfirmDeleteModal.razor` renders `<div class="modal" ...>` and `ZB.MOM.WW.Secrets.Ui` ships **no CSS at all** (no wwwroot css, no scoped `.razor.css`). The component silently depends on the host having no opinion about `.modal` — but Bootstrap 5 (linked by OtOpcUa, HistorianGateway, and ScadaBridge's Host wwwroot) defines `.modal { display: none; }` and only shows it when its own JS adds `.show`. The RCL's modal is pure Blazor (`@if (Visible)`) and never gets Bootstrap's class, so it renders permanently hidden. `modal-backdrop-inpage` / `modal-card` / `btn-row` are similarly unstyled. The defect was invisible to every prior check: the family sweep's Playwright gate only exercised **Add secret** (the in-page `SecretEditor`, which uses no modal), and the modal's `@onclick` handlers are fully functional — forcing `el.style.display='block'` makes the whole delete flow work (that is how the probe secrets were cleaned up). ## Suggested fix (in `ZB.MOM.WW.Secrets.Ui`) 1. Rename the classes to a component-scoped vocabulary (`zb-secrets-modal`, `zb-secrets-modal-backdrop`, `zb-secrets-modal-card`) so no host framework can collide. 2. Ship the styles with the RCL — scoped `ConfirmDeleteModal.razor.css` (bundles into the consumer's `*.styles.css` automatically) covering overlay positioning, backdrop, and card, consistent with the Theme tokens. 3. Add a Playwright/bUnit check that the modal is actually **visible** after clicking Delete (computed `display != none`), on a host page that links Bootstrap — the collision only reproduces there. ## Verification recipe On any Bootstrap host (e.g. OtOpcUa docker-dev :9200): create a secret, click its row **Delete**, assert `[data-testid="delete-modal"]` is visible and `[data-testid="confirm-delete"]` is clickable without JS style forcing. Rig states: both rig probe secrets (`tmp/modal-probe` on :9200, `sb22/audit-bridge-proof` on :9001) were force-deleted through the real `ConfirmAsync` path, so stores are clean.
Author
Owner

FIXED in Secrets.Ui 0.2.3 (scadaproj main @ 62ab8f6; 5 packages published to the Gitea feed 2026-07-19).

Fix, entirely inside the RCL so a package bump repairs every host with no host-side changes:

  • class vocabulary renamed to zb-secrets-modal / -backdrop / -card / -title — no host framework selector can match the elements;
  • the component emits its own <style> block alongside the markup (Theme tokens with fallbacks). Scoped .razor.css was deliberately rejected: only ScadaBridge links a scoped-CSS bundle — OtOpcUa, HistorianGateway and MxGateway never link {App}.styles.css, so isolation CSS would silently fail to load on 3 of 4 hosts, recreating the exact defect class;
  • data-testids unchanged, so existing gates keep their selectors.

Regression pins (bUnit): no Bootstrap-reserved class name in the rendered markup, and the component ships a style block that both positions the overlay and gives it a display mode. Suite 182 pass / 0 fail.

Live-verified on a Bootstrap host (ScadaBridge local cluster, bumped to 0.2.3 @ 63510778): created a probe secret, clicked row Delete[data-testid="delete-modal"] now VISIBLE with computed display=flex, class="zb-secrets-modal", zero JS style forcing — confirmed through the real ConfirmAsync, row gone, store clean. Renders as a proper centered card over a dimmed backdrop.

Remaining rollout: OtOpcUa (0.2.2), HistorianGateway and MxGateway (0.2.1) pick the fix up on their next Secrets bump.

**FIXED in Secrets.Ui `0.2.3`** (`scadaproj main @ 62ab8f6`; 5 packages published to the Gitea feed 2026-07-19). Fix, entirely inside the RCL so a package bump repairs every host with no host-side changes: - class vocabulary renamed to `zb-secrets-modal` / `-backdrop` / `-card` / `-title` — no host framework selector can match the elements; - the component emits its own `<style>` block alongside the markup (Theme tokens with fallbacks). Scoped `.razor.css` was deliberately **rejected**: only ScadaBridge links a scoped-CSS bundle — OtOpcUa, HistorianGateway and MxGateway never link `{App}.styles.css`, so isolation CSS would silently fail to load on 3 of 4 hosts, recreating the exact defect class; - `data-testid`s unchanged, so existing gates keep their selectors. Regression pins (bUnit): no Bootstrap-reserved class name in the rendered markup, and the component ships a style block that both positions the overlay and gives it a display mode. Suite 182 pass / 0 fail. **Live-verified on a Bootstrap host** (ScadaBridge local cluster, bumped to `0.2.3` @ `63510778`): created a probe secret, clicked row **Delete** — `[data-testid="delete-modal"]` now VISIBLE with `computed display=flex`, `class="zb-secrets-modal"`, **zero JS style forcing** — confirmed through the real `ConfirmAsync`, row gone, store clean. Renders as a proper centered card over a dimmed backdrop. Remaining rollout: OtOpcUa (`0.2.2`), HistorianGateway and MxGateway (`0.2.1`) pick the fix up on their next Secrets bump.
Author
Owner

Rollout complete 2026-07-19 — all four apps on 0.2.3, pushed:

App Commit Live modal gate
ScadaBridge 63510778 PASS (:9001)
OtOpcUa lmxopcua master @ be87ddeb PASS (:9200, rig rebuilt)
HistorianGateway historiangw main @ 7452eb4 PASS (:5220, container rebuilt)
MxGateway mxaccessgw main @ 8f7ee49 n/a — Bootstrap-free, never affected (suite zero-delta)

Gate = create probe secret → row Deletedelete-modal visible (display=flex, zb-secrets-modal) with zero JS forcing → confirm → row gone, store clean.

Side fix while verifying: HG's whole test suite had been unbuildable under warnings-as-errors from the pre-existing AngleSharp NU1902 (transitive via bunit) — fixed with a test-only pin to 1.5.2 (historiangw 6bc005d; note 1.3.1 is still in the advisory range).

**Rollout complete 2026-07-19 — all four apps on `0.2.3`, pushed:** | App | Commit | Live modal gate | |---|---|---| | ScadaBridge | `63510778` | ✅ PASS (:9001) | | OtOpcUa | `lmxopcua master @ be87ddeb` | ✅ PASS (:9200, rig rebuilt) | | HistorianGateway | `historiangw main @ 7452eb4` | ✅ PASS (:5220, container rebuilt) | | MxGateway | `mxaccessgw main @ 8f7ee49` | n/a — Bootstrap-free, never affected (suite zero-delta) | Gate = create probe secret → row **Delete** → `delete-modal` visible (`display=flex`, `zb-secrets-modal`) with zero JS forcing → confirm → row gone, store clean. Side fix while verifying: HG's whole test suite had been unbuildable under warnings-as-errors from the pre-existing AngleSharp NU1902 (transitive via bunit) — fixed with a test-only pin to 1.5.2 (`historiangw 6bc005d`; note 1.3.1 is still in the advisory range).
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/scadaproj#2