Files
scadaproj/admin_ui_cleanup.md
T

188 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```css
.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-group`s 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.