Files
scadaproj/admin_ui_cleanup.md
T

12 KiB
Raw Blame History

Admin-UI cleanup playbook (ZB.MOM.WW.Theme + Bootstrap Blazor apps)

A reusable sweep recipe for the family's Blazor Server admin UIs, distilled from two completed rounds:

  • ignitionoee router — ../ignitionoee/docs/plans/2026-08-05-router-ui-sweeps.md (+ its render-validation companion plan)
  • OtOpcUa AdminUI — ../OtOpcUa/docs/plans/2026-08-10-adminui-ui-sweeps.md

Both apps sit on ZB.MOM.WW.Theme 0.3.1 + self-hosted Bootstrap 5.3, and both sweeps found the same classes of defect with different instances. Run the discovery phase before assuming which instances your app has — the router had ghost layout classes and the OtOpcUa AdminUI had none, but OtOpcUa had a dead scoped-CSS bundle the router didn't. Neither app has bUnit; the final gate is always a live browser pass.


1. Discovery — four parallel read-only scans

Run these as parallel read-only agents (or by hand) and write the inventories down with file:line before changing anything. The inventories become the per-batch fix briefs.

1a. Foundation / CSS audit

The theme ships less than pages assume. Check, in order:

  1. Shipped-vs-used class matrix. Extract every CSS class used in *.razor markup and check each against (a) the theme package's staticwebassets/css/*.css (~/.nuget/packages/zb.mom.ww.theme/<ver>/), (b) Bootstrap, (c) the app's own stylesheet, (d) page <style> blocks. Anything defined nowhere is a silent no-op — the browser ignores it without any error. (Router: 13 ghost classes from the mockups' theme.css that 0.3.1 never shipped. OtOpcUa: zero — it was authored against what ships.)
  2. Is the scoped-CSS bundle linked? If any *.razor.css exists, the build emits <App>.bundle.scp.css — but nothing links it automatically when the head is hand-authored. Compare the <link> list in App.razor against obj/…/scopedcss/bundle/. OtOpcUa's ContextMenu.razor.css built, served 200, and was referenced by nothing — every context menu rendered in-flow (no position:fixed, no backdrop) for the component's whole life.
  3. Phantom CSS custom properties. Grep every var(--…) in markup/inline styles against the tokens actually defined in the theme + app CSS. An undefined token makes the declaration invalid at computed-value time — silently dropped, no console error. A fallback argument (var(--x, #fff)) hides the defect. (OtOpcUa: var(--alert) ×13 — the theme's red family is --bad* — so intended red error tints stayed amber; var(--surface-2) likewise dead.)
  4. Button sizing. Theme 0.3.1 ships no .btn rule, so Bootstrap's hard-coded --bs-btn-font-size: 1rem applies against the theme's 0.9rem body. Count how many buttons carry btn-sm vs full-size; modal feet and shared form shells are where full-size accumulates.
  5. Dark scheme: don't chase it. Theme 0.3.1 is light-only (no prefers-color-scheme / data-bs-theme anywhere). Fix any comment claiming otherwise.

1b. Button inventory

Quick tells: grep -rn "btn-group" Components/ returning zero means nothing is grouped (both apps' idiom was d-flex gap-2); also grep for &middot; separators between actions, btn-link p-0 on things that do rather than navigate, py-0/inline width: sizing hacks.

Inventory, with file:line:

  • Adjacent related buttons that should be one btn-group: page-head action pairs, modal/panel feet (Save/Cancel, Cancel/Confirm), pagers, browse-toolbar pairs, inline confirm pairs.
  • Row actions styled as links (<a href="#"> or btn-link text-links) → real btn btn-sm btn-outline-secondary, destructive → btn-outline-danger, grouped when ≥2 in a cell. Distinguish navigation from action: a link that routes somewhere stays a link; a link that mutates state becomes a button. Both directions can coexist in one row (OtOpcUa's UnsTree: keep ⚙ settings/Open as links, convert + Area/Edit/Delete to buttons).
  • Arm→confirm flows (click Delete → Confirm/Cancel appears): list them; the fix is restyle-only and must not collapse the two-step.
  • Size mismatches within one visual group (btn-sm beside full-size).

1c. Prose inventory (DELETE / KEEP / RELOCATE rubric — binding)

  • DELETE: unconditional documentation blocks — always-rendered banners explaining how a feature works, config-mode walkthroughs, "how X is decided" sections, column-meaning tables. These belong in docs/, not on the page. Verify each fact is already docs-covered (grep for distinctive phrases); if it is, plain delete.
  • RELOCATE-then-delete: facts stated nowhere else. Land them in the right doc file first, then delete the page copy — same ordering both sweeps used (foundation/doc task before page batches).
  • KEEP: state banners (empty/error/truncation/live status), form hints (form-text, field hints — they render only in form context), visual-encoding legends that are the only decoder of a chip/graphic, workflow cross-links, expander-gated notes.
  • Hunt stale claims while you're in there — both sweeps found prose that was actively wrong, not just verbose: milestone labels for shipped features ("F8/F9 pending", "(F3)", "arriving in Batch 2/3"), architecture claims superseded by later phases, dead hyperlinks to repo paths the app doesn't serve, and config keys that don't exist (prose said Authentication:Ldap, the code's SectionName said Security:Ldap — always verify cited key names against the options class). Wrong facts outrank style: fix or delete them even if you skip everything else.

1d. Tree tables — check the data shape before porting the component

The router built a generic TreeTable + TreeVisibility (depth-ordered flat ITreeRow list, forward-scan visibility — reference implementation in ../ignitionoee/router/src/ZB.MOM.Oee.Ui/Components/Shared/TreeTable.razor + ViewModels/TreeRows.cs) and adopted it on two pages. OtOpcUa rejected it entirely after the scan. The rubric:

  • Candidates: tables flattening real hierarchy through hacks — a repeated "SITE / AREA / LINE" path-prefix column, hand-rolled rowspan grouping, indent-by-padding rows, faked full-width group-header rows.
  • Not candidates: rank-ordered lists and fact/audit logs (a tree destroys load-bearing ordering), tables already grouped per parent (one table per group), rows that are all roots.
  • Nav/picker trees are a different animal. Lazy-loading navigation trees with context menus or selection (OtOpcUa has four) don't fit the flat-list TreeTable model (no columns, lazy expansion) — leave them alone. Duplicated expander scaffolding across nav trees is not, by itself, reason to unify in a repo with no component test harness.

If no adoption justifies the component, don't port it — an unused component is an orphan.


2. How to fix

Foundation first (one task, own files, everything else references it by name)

CSS class names aren't compile-checked, so batch agents can reference foundation classes before the foundation branch merges — all branches build independently and compose at merge.

  1. App stylesheet loaded after <ThemeHead /> so it wins the cascade (create one if the app lacks it; the router created app.css, OtOpcUa extended site.css).

  2. Button sizing as Bootstrap CSS-variable overrides — never redefine box properties wholesale (btn-group seam/radius machinery depends on them). The block both apps now carry:

    .btn { --bs-btn-font-size: .85rem; --bs-btn-padding-y: .35rem; --bs-btn-padding-x: .8rem; --bs-btn-line-height: 1.4; }
    .btn-group-sm > .btn, .btn-sm { --bs-btn-font-size: .78rem; --bs-btn-padding-y: .25rem; --bs-btn-padding-x: .6rem; }
    

    This normalizes every unsized button app-wide without touching call sites. Upstreaming candidate: this block is now duplicated in ignitionoee app.css and OtOpcUa site.css; when a ZB.MOM.WW.Theme release ships a .btn rule, delete the local copies (both files carry a header comment saying exactly this).

  3. Phantom tokens → a real class, not a token alias. OtOpcUa's pattern: .notice-alert { color: var(--bad); background: var(--bad-bg); border-color: var(--bad-border); } then migrate the inline style="border-color:var(--alert)" hacks to the class in the page batches. Mirror how the theme's own variant classes (e.g. .chip-alert) use the tokens.

  4. Link the scoped bundle if 1a-2 found it dead: <link rel="stylesheet" href="_content/<AppNamespace>/<AppNamespace>.bundle.scp.css"/> — verify the exact asset name from the build output, not by guessing.

  5. Shared class for repeated inline sizing (e.g. .tree-twist { flex: 0 0 18px; width: 18px; } replacing four trees' style="width:18px" expanders).

  6. Relocate the orphaned prose facts into docs (from 1c) in this same task, so page deletions never race the relocation.

Button conventions (page batches)

  • <div class="btn-group" role="group"> (btn-group btn-group-sm when members are small); fold per-button btn-sm into the group class; drop ms-2/me-1 spacers between members.
  • Mixed <a class="btn"> + <button> members are valid Bootstrap — group them (keep download etc. attributes verbatim).
  • A group whose conditional member doesn't render is fine — keep @if inside the group.
  • Deliberately separated members stay outside the group (the ms-auto Delete in a form foot); inputs interleaved in a run split it into two groups (never put a form-control inside).
  • Destructive actions → btn-outline-danger; the armed/confirm state may go solid btn-danger.
  • Preserve behavior byte-for-byte: every @onclick, disabled, binding, AuthorizeView gate, and arm→confirm flow unchanged — wrappers, classes, and prose only. Grep the diff for @onclick|disabled=|href=|type="submit" appearing identically on both sides.
  • Respect existing "buttons-not-anchors" comments (Blazor's blazor.web.js navigation interceptor is why some tree labels are buttons) — never convert those to links.

Execution pattern (what actually worked, twice)

  • Discovery scans → plan doc with the full inventories → one foundation agent + N page-batch agents on disjoint file sets, all parallel; shared components (form shells, collection editors) are the biggest lever — one file can fix a dozen forms.
  • Briefs must carry exact file:line inventories and the instruction "verify by reading before editing; refuse items the markup contradicts." Both sweeps had agents correctly refuse brief items (buttons in mutually exclusive @if branches can't be grouped; toolbars the inventory over-counted).
  • Merge foundation first, then batches; run the full build + test suite on the merged tree even if every branch was green in isolation (parallel branches compose only at the full build).
  • Verification greps on the merged tree: zero remaining phantom-token usages, zero remaining &middot; action separators / inline width: hacks, btn-group count roughly matching the inventory, the bundle link present.

Live browser gate (mandatory — these apps have no bUnit)

Rebuild/redeploy the app's dev rig (all instances behind the load balancer — round-robin serves you the stale one otherwise), then walk:

  1. Computed-style probes (one javascript eval): a .btn's font-size vs body; the phantom-token replacement class resolving to real colors; the scoped bundle present in document.styleSheets.
  2. Every page leads with data — no unconditional documentation above the fold.
  3. Seamed btn-groups where the inventory says; destructive members red.
  4. One arm→confirm flow per style: arm → confirm UI appears → Cancel (never complete a real write on shared rigs); verify disarm.
  5. Anything the scoped bundle resurrected (context menus float at the cursor, backdrops cover).
  6. Legends/hints/empty states from the KEEP list still render.

Record PASS/FAIL per check in the plan or a recipe doc; CSS-only fixes may land in-session, anything structural gets recorded instead.