# AdminUI density/layout sweep (§1d follow-through) — 2026-08-10 Follow-through of the `/hosts` density rework: the §1d scan (four parallel read-only agents, 62 files) applied to the rest of the AdminUI. Playbook: `../scadaproj/admin_ui_cleanup.md` §1d + §2 "Density fixes". Prior context: `2026-08-10-adminui-ui-sweeps.md` (structure sweep) and the `/hosts` rework commit `34d60c41`. ## Scan verdict - **Zero unguarded `[..N]` slices** anywhere (all existing slices are length-guarded or Guid-fixed). - Cluster pages: no A/B/D defects at all; 3 C items. - The dominant class is **C — unbounded backend/free text** (~17 sites), concentrated on the fault-triage surfaces (`/alerts`, `/fleet`, `/script-log`, alarms-historian, DriverStatusPanel). - Two structural defects: `Scripts.razor` (inline `
` source dump + identity slam — the /hosts shape) and `EquipmentPage.razor` (only authoring page with no panel idiom + bare RawPath column). - Deliberate designs kept: `RawManualTagEntryModal` colspan config sub-row (documented, inside `modal-xl` — a fix means stacked modals), `VirtualTagModal` inline Monaco card (collapsed by default, own card boundary). - Style-only residual (not §1d, not in this sweep): `Deployments.razor` is off the panel idiom (bare `

` + Bootstrap table). ## Task 1 — mechanical truncation sweep (Abbreviate + title) All `DisplayText.Abbreviate(value, N)` + full text on `title`; behavior byte-for-byte. | File:line | Value | N | |---|---|---| | `Pages/Alerts.razor:84` | alarm `Message` | 80 | | `Pages/Fleet.razor:92` | `FailureReason` | 80 | | `Pages/ScriptLog.razor:71` | log `Message` (mono) | 120 | | `Pages/AlarmsHistorian.razor:41` | drain `LastError` | 80 | | `Shared/Drivers/DriverStatusPanel.razor` | op-result chip — truncate in `ShowOpResult` choke point, `title` on chip; also `:41` SignalR `_error` | 60 | | `Pages/Certificates.razor:77-78` | Subject / Issuer DNs | 48 | | `Pages/RoleGrants.razor:65,96,24` | LDAP group DNs, SearchBase | 48 | | `Pages/Clusters/ClusterAcls.razor:88` | ACL `Notes` | 60 | | `Pages/Clusters/ClusterOverview.razor:49,100,102` | cluster `Notes` (120), `Host` (30), `ApplicationUri` (40) | — | | `Pages/Clusters/ClusterRedundancy.razor:128` | `ApplicationUri` | 40 | | `Pages/Account.razor:63` | group-DN chips | 40 | | `Pages/Clusters/ClusterAudit.razor:55-57` | add `title` to the already-ellipsized DetailsJson cell | — | | `Shared/Drivers/Pickers/SqlAddressPickerBody.razor:208-211` | composed TagConfig JSON `` | 64 | | `Shared/Drivers/Pickers/SqlAddressPickerBody.razor:159-162` | `_openError` → adopt the sibling truncated-chip + `title` pattern | 60 | | `Shared/Drivers/DriverTagPicker.razor:19-22` | footer `CurrentAddress` | 60 | | Picker Result `` lines (AbCip:75, TwinCAT:64, OpcUaClient:57, Galaxy:95, FOCAS:109) | browsed address | 64 | ## Task 2 — Scripts page: `
` expander → source dialog + two-line identity `Pages/Scripts.razor`: replace the per-row inline `
`/`
` source dump with the
Bootstrap modal pattern (`modal-backdrop fade show` + `modal fade show d-block`, `modal-lg`,
one at a time); list rows get the stacked identity (name first line, mono id + hash beneath,
chips with the name). Hold only the open row's `ScriptId`; re-resolve from current data every
render (self-closes if the script vanishes).

## Task 3 — EquipmentPage: adopt the panel idiom + bound the RawPath column

`Pages/Uns/EquipmentPage.razor`: wrap each tab body in `
` + `panel-head` (Details tab: Identity and OPC 40010 as two panels, dropping the `
`), matching ScriptEdit's shells; `Abbreviate(r.RawPath, 48)` + `title` at :166. Behavior byte-for-byte — tab switching, all bindings, buttons, modals unchanged. ## Verification Merged-tree full build + AdminUI.Tests; live gate on docker-dev (both centrals rebuilt) with at least one faulted/erroring row on screen (§1d realistic-data rule): /alerts, /fleet, /scripts dialog open/close, equipment page tabs, Sql picker footer.