docs: admin_ui_cleanup.md — reusable admin-UI sweep playbook (from the ignitionoee router + OtOpcUa AdminUI rounds)
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# 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 `·` 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
|
||||
`·` 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.
|
||||
Reference in New Issue
Block a user