Files
scadalink-design/docs/plans/2026-05-12-ui-audit.md
Joseph Doherty ff5f5a10ef docs(ui): UI audit findings (2026-05-12)
Audit of every page in CentralUI against the Sites.razor card-grid
pattern, the no-third-party-UI-libs constraint, and accessibility
basics. Findings + per-page severity + suggested implementation
order live in docs/plans/. Implementation follows in subsequent commits.
2026-05-12 03:31:54 -04:00

32 KiB

ScadaLink Central UI — Design & UX Audit

Date: 2026-05-12 Branch at audit time: feature/templates-folder-hierarchy (after Sites.razor redesign, commit 0805e18) Scope: All Razor pages, layout, and shared components in src/ScadaLink.CentralUI. Reference pattern: src/ScadaLink.CentralUI/Components/Pages/Admin/Sites.razor — 2-column responsive card grid, header flex row, kebab menus, search filter, Bootstrap collapse for noisy details, @key= on iterated cards, "No X match the filter." and empty-state CTAs.

Constraints (recap)

  • Blazor Server + Bootstrap 5 only. No third-party component frameworks (no MudBlazor / Radzen / Blazorise / Syncfusion).
  • Clean, corporate, internal-use aesthetic. Not flashy.
  • Form pages: vertical stacking; read-only fields first; subsections stacked; buttons at bottom.
  • Accessibility: aria-labels on icon buttons; labels paired with inputs; semantic headings; never use color as the only state cue.

Severity summary

Severity Count Pages
High 7 LdapMappingForm · DataConnections (header/a11y) · SharedScripts · ExternalSystems · TemplateEdit · DebugView · EventLogs
Medium 11 LdapMappings · ApiKeys · DataConnections · DataConnectionForm · ApiKeyForm (partial) · Templates · Topology · Deployments · Dashboard · Health · ParkedMessages · AuditLog · MainLayout / NavMenu · ConfirmDialog · Toast · global CSS
Low 7+ Most form pages (TemplateCreate, ExternalSystemForm, SharedScriptForm, DbConnectionForm, ApiMethodForm, NotificationListForm) · Login error feedback · NotAuthorizedView · LoadingSpinner contrast · DataTable clear-button

Suggested implementation order (high impact / low risk first):

  1. Shared shell fixes (ConfirmDialog scroll-lock + Escape + default button color, Toast aria-live + custom delay, NavMenu scroll container, login vertical centering) — these unblock everything else and are mostly small.
  2. List-page pattern roll-out: apply the Sites.razor card grid + search + kebab template to LdapMappings, ApiKeys, SharedScripts. These are mechanical.
  3. DebugView guardrails: scroll-lock, max-row cap, aria-live, filter — this is high-severity and isolated.
  4. EventLogs: message expand, pagination clarity, filter accessibility.
  5. ExternalSystems + TemplateEdit refactors — biggest scope, leave for last because they need design discussion before implementation.

Cross-cutting findings (apply to many pages)

These show up everywhere. Fix at the pattern level first, then tour every page once to apply:

  1. <h4> page title in a flex header. Sites.razor sets the standard at line 16. Currently Templates (<h6>), Topology (<h6>), Dashboard (<h3>), and most form pages mix levels. Adopt <h4 class="mb-0"> inside d-flex justify-content-between align-items-center mb-3.
  2. Search input above the list. max-width: 320px, bound to _search with @bind:event="oninput", plus the "No X match the filter." inline message. Missing on: LdapMappings, ApiKeys, SharedScripts, EventLogs, ParkedMessages (per-site only), AuditLog.
  3. Kebab (⋮) menu for less-frequent actions. Edit stays as a primary button; Delete/Disable/Deploy move into the dropdown. Missing on: LdapMappings, ApiKeys, SharedScripts, TemplateEdit member rows, ParkedMessages.
  4. @key="entity.Id" on iterated rows / cards. Prevents Bootstrap collapse state leaks (the bug caught in smoke on Sites). Apply anywhere @foreach renders elements with Bootstrap stateful classes (show, collapsed, active).
  5. State badges must not rely on color alone. Add either icon + text or aria-label="State: …". Affected: Health node Online/Offline, Topology Stale, Deployments row colors, DebugView Quality / Alarm State, AuditLog action badges.
  6. TimestampDisplay component consistency. EventLogs / ParkedMessages / AuditLog use it; Health and DebugView format inline. Pick the component, give it a single rendering of "HH:mm:ss UTC" or relative+absolute, retrofit everywhere.
  7. Empty-state CTA when count is 0. Sites.razor lines 53-60 are the template. Missing on: SharedScripts, Templates (tree), ExternalSystems tabs, ParkedMessages, AuditLog.
  8. aria-label on icon-only buttons (, 📋, copy, expand/collapse). Almost universally missing today.
  9. Truncate-and-expand pattern. AuditLog has the cleanest pattern (View toggle for state JSON). Apply to long message strings (EventLogs, ParkedMessages, Deployments errors) instead of mid-string CSS truncation.

Admin section

LdapMappings.razor — /admin/ldap-mappingsMedium

File: src/ScadaLink.CentralUI/Components/Pages/Admin/LdapMappings.razor What it does: Lists LDAP group → role mappings with inline Edit/Delete and Site Scope hints.

Issues

  1. Consistency: Header (line 12) lacks the Sites flex layout + Bulk actions dropdown next to the primary Add button.
  2. Density: 5-column table; "Site Scope Rules" cell jams multiple badges into a narrow column.
  3. Consistency: No search filter. Sites uses one at lines 67-69.
  4. Consistency: Edit + Delete rendered as twin buttons in the row; Sites uses kebab.
  5. Other: "Site Scope Rules" preview in the row + the "(manage on edit page)" hint creates a confusing duality — the list page promises something it can't deliver.

Recommendations

  1. Add header flex layout + search input.
  2. Replace Edit/Delete pair with Edit button + dropdown containing Delete.
  3. Either drop the Site Scope column from the list entirely (show a n rule(s) badge instead) or expand it into a collapse panel on the row.
  4. If keeping table layout, add @key="m.Id".

LdapMappingForm.razor — /admin/ldap-mappings/create and /{Id}/editHigh

File: src/ScadaLink.CentralUI/Components/Pages/Admin/LdapMappingForm.razor What it does: Create/edit a single mapping, plus a secondary panel for Site Scope Rules in edit mode.

Issues

  1. Form-layout: Two distinct sub-forms on one page (mapping basics + scope rules) with no visual separation. Scope rules only become editable after Save, but the UI doesn't communicate that workflow.
  2. Hierarchy: Both sections use <h6> inside card-title; no primary/secondary hierarchy.
  3. Form-layout: Scope-rule entry uses a nested table inside the card; visually heavy.
  4. Accessibility: Role <select> has no aria-describedby / help text explaining why "Deployment" surfaces the scope rules section.

Recommendations

  1. Restructure: top card "Mapping" stacked vertically (Name, LDAP Group, Role, [Save]); below it, a card "Site Scope Rules" that's disabled-with-explanation in create mode and editable in edit mode.
  2. Replace the nested scope-rule table with a tag-style chip list: each scope rule renders as a removable chip; an inline "Add scope rule" form sits below.
  3. Add form-text under Role: "Deployment role: configure site scope below after saving."

DataConnections.razor — /admin/connectionsHigh for header / a11y, Medium overall

File: src/ScadaLink.CentralUI/Components/Pages/Admin/DataConnections.razor What it does: Treeview of sites and their data connections with context menu CRUD.

Issues

  1. Hierarchy: Page title is <h6> (line 24). Promote to <h4> with flex header to match Sites.
  2. Consistency: Inline btn-group with Refresh / Expand / Collapse buttons next to search; visually busy. Sites uses Bulk actions dropdown + Add button only.
  3. Accessibility: Tree node kebab toggles lack aria-label="More actions for {name}".
  4. Other: Right-click context menu has no visible hover affordance — easy to miss.
  5. Other: When search returns no matches, the tree silently collapses; no empty-state message.

Recommendations

  1. Promote heading, adopt flex header. Move Expand/Collapse into a Bulk actions dropdown; drop Refresh (navigation reload covers it).
  2. Add visible kebab on tree-node hover so the context menu is discoverable.
  3. Add aria-label to every kebab toggle (interpolate the node name).
  4. Add "No connections match the filter." inline when search clears the tree.

DataConnectionForm.razor — /admin/connections/create and /{Id}/editMedium

File: src/ScadaLink.CentralUI/Components/Pages/Admin/DataConnectionForm.razor What it does: Create/edit a connection with primary + optional backup endpoint editors (OPC UA only today).

Issues

  1. Form-layout: Site field is disabled in edit mode but rendered as a disabled <select> with no read-only styling cue beyond gray.
  2. Hierarchy: "Backup endpoint" <h6> uses border-bottom; primary endpoint has no parallel heading. Hierarchy is one-sided.
  3. Density: "Add Backup Endpoint" button buried inside the card with no signposting that backup is optional.
  4. Accessibility: No form-text on Primary Endpoint / Site / failover knobs.

Recommendations

  1. Use <input class="form-control-plaintext" readonly> for the Site field in edit mode and add a small explanatory line ("Site is locked after creation").
  2. Mirror the heading pattern: both Primary and Backup get <h6> headers; Backup also gets a clear "Optional" badge.
  3. Add form-text help under each tuning knob (PublishingIntervalMs, SamplingIntervalMs, FailoverRetryCount, etc.).

ApiKeys.razor — /admin/api-keysMedium

File: src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeys.razor What it does: Lists API keys with Edit / Disable-Enable / Delete actions; masked key value.

Issues

  1. Consistency: No search filter.
  2. Density: 5-column table; Status column is redundant with the Disable/Enable button.
  3. Consistency: Three buttons in the Actions cell (Edit / Disable / Delete) — should be Edit + kebab.
  4. Other: No @key="k.Id" on rows.

Recommendations

  1. Add search filter and @key.
  2. Drop the Status column; let the kebab item read "Disable" or "Enable" depending on state.
  3. Either keep the table and adopt the kebab pattern, or move to the Sites card grid — for ~5 keys per environment the table is fine; for 50+ the card grid would scan better.

ApiKeyForm.razor — /admin/api-keys/create and /{Id}/editLow

File: src/ScadaLink.CentralUI/Components/Pages/Admin/ApiKeyForm.razor What it does: Create an API key (showing the secret once) or rename an existing one.

Issues

  1. Form-layout: Header has conditional "Back to API Keys" vs "Back" text.
  2. Other: Copy button on the one-shot secret reveal is wired to a comment / no-op.
  3. Density: Form is one field but wrapped in card-inside-card.

Recommendations

  1. Fixed header: ← Back · Add / Edit API Key.
  2. Implement the copy via IJSRuntime + navigator.clipboard.writeText (mirror Sites.razor's CopyAsync).
  3. Remove redundant card nesting; render the input + buttons directly in <div class="container-fluid mt-3">.

Design section

Files discovered:

Components/Pages/Design/Templates.razor                @page /design/templates
Components/Pages/Design/TemplateCreate.razor           @page /design/templates/create
Components/Pages/Design/TemplateEdit.razor             @page /design/templates/{Id:int}
Components/Pages/Design/SharedScripts.razor            @page /design/shared-scripts
Components/Pages/Design/SharedScriptForm.razor         @page /design/shared-scripts/{create|edit}
Components/Pages/Design/ExternalSystems.razor          @page /design/external-systems
Components/Pages/Design/ExternalSystemForm.razor       @page /design/external-systems/{create|edit}
Components/Pages/Design/DbConnectionForm.razor         @page /design/db-connections/{create|edit}
Components/Pages/Design/ApiMethodForm.razor            @page /design/api-methods/{create|edit}
Components/Pages/Design/NotificationListForm.razor     @page /design/notification-lists/{create|edit}

Templates.razor — Medium

What it does: Folder-tree view of templates with context-menu CRUD.

Issues

  1. Hierarchy: Page title is <h6> (line 53) — should be <h4> in flex header.
  2. Consistency: btn-group-sm of outline buttons for Expand/Collapse — push these into a Bulk actions dropdown.
  3. Accessibility: Context-menu buttons (lines 271-288) lack aria-label.
  4. Density: Treeview height is hardcoded calc(100vh - 160px) with no scroll affordance.
  5. Other: No breadcrumb when an edit page navigates away from the tree context.

Recommendations

  1. Promote heading, adopt flex header pattern.
  2. Move Expand/Collapse into the Bulk actions dropdown.
  3. Add aria-labels on every context-menu button (interpolate node name).
  4. Add a top breadcrumb on TemplateEdit so users know which folder they're editing inside.

SharedScripts.razor — High

What it does: Table of shared scripts with name, code preview, parameters, returns.

Issues

  1. Consistency: Table instead of card grid — and code preview is rendered as truncated monospace inline, which is unreadable beyond ~40 chars.
  2. Density: 6 columns (ID, Name, Code preview, Parameters, Returns, Actions). ID is internal-only.
  3. Consistency: No search, no empty-state CTA.
  4. Accessibility: Truncated code preview has no title= tooltip.

Recommendations

  1. Migrate to a card grid (col-lg-6) mirroring Sites: title = Name, body = small code snippet (first 80 chars) + parameter/return counts as chips, footer = Edit + ⋮ Delete.
  2. Drop ID column entirely.
  3. Add search by name + code substring.
  4. Add "No shared scripts configured. Create your first script." CTA.

ExternalSystems.razor — High

What it does: Tabbed hub for External Systems, DB Connections, Notification Lists, Inbound API Methods, SMTP Config, API Keys.

Issues

  1. Density: Six subsections on one page with no search per tab; SMTP form crams 6+ inputs in one row g-2 align-items-end flex row.
  2. Consistency: Tabs use mixed renderings — External Systems / DB / API Methods use tables; Notification Lists and SMTP use cards. Same-level data, inconsistent shape.
  3. Form-layout: SMTP form violates the vertical-stacking rule.
  4. Hierarchy: Subsection headings are <h6> with badge counts — heading level is too small.
  5. Accessibility: Tab buttons lack role="tab" / aria-selected.
  6. Other: No per-tab empty state.

Recommendations

  1. Split SMTP off as a standalone /admin/smtp (it's a single-row global config, not list data).
  2. Unify all tabs on the same card-grid pattern.
  3. Reformat the remaining SMTP page to vertical-stacked fields per feedback_form_layout.
  4. Add role="tablist" / role="tab" / aria-selected and aria-controls on the tab nav.
  5. Add per-tab search + empty-state CTAs.

TemplateEdit.razor — High

What it does: Edit a template's properties plus Attributes / Alarms / Scripts / Compositions in tabs.

Issues

  1. Density: Template Properties card uses a 4-column row; Parent Template renders as form-control-plaintext next to live inputs, then a Save button at col-md-2. Save ends up mid-row instead of at the bottom.
  2. Form-layout: "Add Attribute / Alarm / Script" inline forms use row g-2 align-items-end — the Scripts row stuffs 4 inputs + a textarea horizontally.
  3. Consistency: Card headers inconsistent — some "card-title" h6 inside card-body, some bare h6 above a section.
  4. Hierarchy: Validation result alerts mix strong-heading + bare <li> items.
  5. Accessibility: Lock-state badges render as cryptic single letters "L"/"U" with no aria-label. Tabs lack role="tab" / aria-selected.
  6. Other: Per-row Delete buttons scattered; many tables.

Recommendations

  1. Reflow Template Properties to vertical-stack (col-12 each), put Save at the bottom following the form-layout rule.
  2. Reformat add-forms into a card with stacked col-12 inputs; Scripts gets a full-width Monaco-ish textarea (rows≥10) below the metadata fields.
  3. Replace L/U badges with full text + aria-label: <span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>.
  4. Per-row kebab menu replacing Delete (with future Duplicate / Move options).
  5. Add role/aria-selected to all tab buttons.

TemplateCreate.razor — Low

  1. Use form-control not form-control-sm for the primary Name field.
  2. Replace the &larr; arrow on the Back button with text ← Back and add aria-label="Back to Templates".

ExternalSystemForm.razor — Low

  1. Auth Config field: add a JSON example placeholder matching the chosen AuthType.

SharedScriptForm.razor — Low

  1. Add a small bi-question-circle icon next to Parameters / Return Definition linking to a tooltip with schema reference.
  2. When syntax check fails, surface line/column position in the error message.

DbConnectionForm.razor — Low

  1. Add reassurance text under Connection String: "Stored encrypted; not displayed after save." (only if the back end actually does this; otherwise drop the claim.)

ApiMethodForm.razor — Low

  1. Script textarea bumped from rows=5 to rows≥10.
  2. Add JSON example placeholders for Params and Returns.

NotificationListForm.razor — Low

  1. Resize the Name input to form-control (not form-control-sm).
  2. Recipients <thead class="table-dark">table-light for consistency.

Deployment section

Files discovered:

Components/Pages/Deployment/Topology.razor             @page /deployment/topology (and /deployment/instances)
Components/Pages/Deployment/Deployments.razor          @page /deployment/deployments
Components/Pages/Deployment/DebugView.razor            @page /deployment/debug-view
(+ InstanceCreate, InstanceConfigure, CreateAreaDialog, MoveAreaDialog, MoveInstanceDialog)

Topology.razor — Medium

  1. Hierarchy: <h6> page title (line 63) — promote to <h4> in flex header.
  2. Accessibility: Expand / Collapse / Refresh / Search / tree-kebab buttons all lack aria-label. Inline rename input has no label.
  3. Live-data UX: No "pause live updates" toggle; tree can repaint while user is renaming or moving a node.
  4. Density: Instance counts footer text — could be a summary card above the tree.
  5. State cues: Stale badge is yellow-only; pair with text or icon.
  6. Consistency: Diff modal is hand-rolled Bootstrap modal markup — should be a reusable <DiffDialog> mirroring <ConfirmDialog>.

Recommendations

  1. Promote heading, adopt flex header.
  2. Add aria-labels everywhere (treat the kebab and rename input as the priority).
  3. Add a "Live updates: on/off" toggle button next to Refresh; pause auto-refresh during edits.
  4. Move counts to a small summary card above the tree.
  5. Pair Stale badge with aria-label="State: Stale" and a 🟡 dot or "STALE" text.
  6. Extract <DiffDialog> into Components/Shared/.

Deployments.razor — Medium

  1. Density: 8 columns (Deployment ID, Instance, Status, Deployed By, Started, Completed, Revision, Error). Both Deployment ID and Revision are truncated hashes; Error can be a stack trace.
  2. Live-data UX: Auto-refresh runs every 10s with no pause control — if a user is reading an error message, the row can swap underneath them.
  3. Consistency: Summary cards use col-md-3 only (no col-sm-6 fallback for tablet); cards are styled differently from Sites.
  4. Accessibility: Spinner inside the status badge has no role="status" / aria-label. "Auto-refresh: 10s" text is decorative, not a control.
  5. State cues: Row colors (table-danger, table-info) without an icon or stripe.
  6. Other: Empty state is a single line of text.

Recommendations

  1. Collapse Error column into a View error button that pops a <DiffDialog>-style modal (or inline collapse row).
  2. Add Live updates: 10s [pause] toggle.
  3. Make summary cards col-lg-3 col-md-6 col-12.
  4. Add aria-labels on the spinner and the toggle.
  5. Add border-start border-3 border-danger or icon to failed rows.
  6. Either fold Deployment ID + Revision into one cell or hide one behind the detail modal.

DebugView.razor — High

  1. Live-data UX: No scroll-lock on the streaming tables. Auto-scroll behavior is implicit. No max-row cap → tab can balloon in memory.
  2. Live-data UX: Timestamps shown to milliseconds; noisy at sustained update rates.
  3. Live-data UX: No stream filter (e.g., "only alarms with state=Active") — once subscribed, you watch everything.
  4. Accessibility: Quality / Alarm State badges are color-only. No aria-live="polite" on the streaming table bodies.
  5. Consistency: "Snapshot received at …" is a tiny muted footer; should be a header-level status strip.
  6. UX risk: Page persists session in localStorage and auto-reconnects on refresh, with no user-visible notice.

Recommendations

  1. Add per-table 🔒 Lock scroll toggle.
  2. Cap rows at e.g. 200; add a Clear button.
  3. Add per-table filter input.
  4. Display timestamps as HH:mm:ss by default; .fff only inside an "Expanded row" view.
  5. Add aria-live="polite" aria-atomic="false" on the streaming table bodies.
  6. Pair every Quality and Alarm State badge with aria-label.
  7. Replace the snapshot footer with a status strip: instance · connection state · last snapshot time.
  8. On auto-reconnect, toast "Auto-reconnected to {instance}" with a Start fresh button.

Monitoring section + Dashboard

Files discovered:

Components/Pages/Dashboard.razor                       @page /
Components/Pages/Monitoring/Health.razor               @page /monitoring/health
Components/Pages/Monitoring/EventLogs.razor            @page /monitoring/event-logs
Components/Pages/Monitoring/ParkedMessages.razor       @page /monitoring/parked-messages
Components/Pages/Monitoring/AuditLog.razor             @page /monitoring/audit-log

Dashboard.razor — Medium

  1. Dashboard UX: It is currently just a user-info card. For a central SCADA console the landing page should show system KPIs first (sites online/offline, errors, queue depths, parked-message count) — the things you'd want to see in <5 seconds.
  2. Hierarchy: <h3> heading; rest of the site is <h4>.
  3. Consistency: Inline style="max-width:500px" instead of Bootstrap utilities.

Recommendations

  1. Repurpose as a "Glance" page: KPI cards across the top (Sites, Errors, Parked Messages, Latest deployments status), a sites-by-health small list, recent audit events.
  2. Move the user-info card to a secondary panel or drop it (it's already in the top-right of the layout).
  3. <h3><h4> for site-wide consistency, replace inline styles with utility classes.

Health.razor — Medium

  1. KPI choices: Sites Online + Sites Offline + Total Sites is redundant; Total Script Errors is global and not actionable. Promote "Sites with active errors" / "Cluster degraded" instead.
  2. Hierarchy: Header is <h4> left-aligned with no flex header; doesn't match Sites.
  3. Density: Per-site cards use a 4-column inner grid that breaks on narrow viewports.
  4. Time format: HH:mm:ss only, no timezone, no relative.
  5. State cues: Online/Offline / Primary/Standby badges are color-only.

Recommendations

  1. Replace "Total Sites" KPI with "Sites with active errors" or "Cluster health %".
  2. Adopt flex header layout.
  3. Reduce per-site card to 2 columns (col-md-6) or wrap each subsection in a collapse à la Sites.razor "Cluster nodes".
  4. Use TimestampDisplay with UTC suffix; consider adding a relative time hint ("3 minutes ago").
  5. Add aria-label and an icon to every Online/Offline/Primary/Standby badge.

EventLogs.razor — High

  1. Density: "Message" column truncates long error strings mid-string with no expand.
  2. Pagination: "Load more" + continuation token, no total count shown.
  3. Filter affordance: 7 filter inputs in one row; "Keyword" label is vague.
  4. Accessibility: Labels are not linked to inputs via for/id; row colors are the primary severity cue.
  5. Time: Uses <TimestampDisplay> — confirm it standardises with the other log pages.

Recommendations

  1. Apply AuditLog's View / Hide toggle pattern for the Message cell.
  2. Switch to numeric pagination ("Page X of Y, N total") or surface a total count next to the Load More button.
  3. Move the filter row into a Bootstrap collapse with label Filter options (n active).
  4. Add id/for pairings, aria-labels, and pair the row color with an icon stripe.
  5. Standardise on TimestampDisplay across all log pages.

ParkedMessages.razor — Medium

  1. Density: Message ID is truncated to 12 chars with no copy or expand affordance.
  2. Density: Error message field can be long; no expand.
  3. Accessibility: Retry / Discard buttons have title= only, no aria-label.
  4. State: No spinner / disabled affordance while a Retry is in flight.

Recommendations

  1. Render Message ID as a <code> with a 📋 Copy button or expand row showing the full ID + error.
  2. Apply AuditLog's expand toggle for error messages.
  3. Add aria-label="Retry message {id}" and aria-label="Discard message {id}".
  4. Replace each action button's normal/disabled state with a small spinner during the action.

AuditLog.razor — Medium

  1. Pagination bug: Next is disabled when _entries.Count < _pageSize; this misfires when the last page has exactly _pageSize rows (will show enabled Next that returns empty).
  2. Filter affordance: 5 filter inputs in one row; no Clear filters button.
  3. Density: Entity ID is a full GUID with no copy / expand.
  4. State expansion: JSON detail has max-height: 200px with no "expand to full size" affordance.
  5. Accessibility: View/Hide button has no aria-label.

Recommendations

  1. Fix the pagination logic: rely on a "has more" flag from the API, not a length compare.
  2. Add a Clear filters button next to the filter row.
  3. Add a copy button or expand-on-click for Entity ID.
  4. Make the JSON detail panel resizable, or open in a <DiffDialog>-style modal when content exceeds 1 KB.
  5. Add aria-label to the toggle (interpolate entry id).

Layout, shared components, global CSS

MainLayout.razor / NavMenu.razor / App.razor

Issues

  1. Responsive: Sidebar is fixed min-width: 220px / max-width: 220px in App.razor lines 13-14. No d-none d-lg-flex or hamburger toggle for narrow viewports. High.
  2. Scrolling: <ul class="nav flex-column flex-grow-1"> has no overflow boundary. If role-driven nav becomes long enough, the footer (username + Sign Out) will scroll off-screen. Medium.
  3. Semantics: Section headers (Admin, Design, …) render as bare <li class="nav-section-header"> — not focusable / not semantic. Medium.
  4. Active state: Active blue (#0d6efd) and hover gray (#343a40) are similar enough to confuse — pair active with a left border or underline. Low.

Recommendations

  1. Wrap the sidebar in d-none d-lg-flex + add a hamburger button in the top bar for <lg viewports. Replace fixed widths with flex-basis: 220px and let it collapse off-canvas on mobile.
  2. Wrap <ul> in <div style="overflow-y:auto; flex:1 1 auto;"> so the footer is always anchored.
  3. Convert section headers to <li role="presentation"><span class="nav-section-header">Admin</span></li> or just <div role="separator" aria-label="Admin section">.
  4. Add border-left: 3px solid var(--bs-primary) to .nav-link.active.

Login.razor — Medium / Low

Issues

  1. Centering: margin-top: 10vh; on the container — on short viewports the card pushes below the fold. Medium.
  2. Validation: No client-side validation feedback for empty fields; only server-side via ?error= query param. Low.

Recommendations

  1. Wrap in <div class="d-flex align-items-center justify-content-center min-vh-100"> for true vertical centering.
  2. Add HTML5 required and :invalid styling; keep the server-side error banner for actual auth failures.

NotAuthorizedView.razor — Low

  1. Wrap in the same centered layout as Login, with the "ScadaLink" brand heading on top — currently feels orphaned.

ToastNotification.razor — Medium

Issues

  1. z-index: Toasts are at z-index: 1090; Bootstrap modal backdrop defaults to 1040 and the modal element itself to 1055. Currently OK, but ConfirmDialog markup doesn't set explicit z-index on the modal element — document the hierarchy or set explicit values.
  2. Auto-dismiss: Hardcoded 5 s. No way to extend for important messages.
  3. Accessibility: role="alert" is set but aria-live="polite" / aria-atomic="true" are missing.

Recommendations

  1. Document the z-index ladder in a comment at the top of the component; set explicit z-index in ConfirmDialog too.
  2. Add [Parameter] public int AutoDismissMs { get; set; } = 6000;.
  3. Add aria-live="polite" aria-atomic="true" to the container.

ConfirmDialog.razor — High / Medium

Issues

  1. Scroll: Backdrop doesn't add overflow: hidden to <body> — the page behind scrolls under the dialog. High.
  2. Keyboard: No Escape-to-close handler. No focus trap. Medium.
  3. Defaults: ConfirmButtonClass defaults to btn-danger — wrong for non-destructive confirms. Medium.

Recommendations

  1. On ShowAsync, JS-interop add overflow:hidden to body; remove on close.
  2. Add @onkeydown="..." for Escape → Cancel; on show, focus the cancel button (or the safer button) and on close return focus to the trigger.
  3. Default ConfirmButtonClass to btn-primary; explicit btn-danger on destructive call sites only.

LoadingSpinner.razor — Low

  1. text-muted on a light background may not meet 4.5:1. Switch to text-secondary.

DataTable.razor — Low

  1. Search input has no clear (✕) button.
  2. Pagination disabled state is on the parent <li> not the button — apply disabled directly + aria-disabled="true".

NewFolderDialog.razor — Low

  1. Uses combined modal + inline background style instead of a separate <div class="modal-backdrop fade show"> like ConfirmDialog. Refactor to match.

TreeView.razor / TreeView.razor.css

  1. Reliance on var(--bs-*) is good; no change.
  2. Same a11y caveats as Topology — hover/focus visuals must reach kebab toggles.

Global CSS — Medium

Issues

  1. Inline: ~60 lines of <style> are inline in App.razor instead of in a wwwroot/css/site.css file.
  2. Theming: Sidebar uses hardcoded hex colors (#212529, #343a40, #adb5bd, #fff); blocks any future light-mode / brand variation work.
  3. Reconnect modal: Uses ad-hoc flex centering; could just be .modal-dialog-centered.

Recommendations

  1. Move inline styles to wwwroot/css/site.css and link in App.razor.
  2. Replace hex with var(--bs-dark) / var(--bs-light) etc.
  3. Use Bootstrap's .modal-dialog-centered for the reconnect overlay.

Cross-cutting strategic recommendations

These are bigger investments that pay back across many pages:

  1. Dialog/Modal service. A single IDialogService that owns z-index stacking, body scroll lock, focus trap, Escape-to-close. Replace per-component ad-hoc backdrops. Fixes ConfirmDialog scroll-lock, focus-trap, and z-index collisions in one stroke; also unblocks the planned <DiffDialog> for Topology and the error-detail modal for Deployments.
  2. Accessibility pass. Adopt a single rule: every icon-only button has aria-label; every state badge is colour + text + icon; every form input has linked label and optional aria-describedby. Most pages need ~5 minutes of edits to comply.
  3. Design tokens via CSS variables. Pull the sidebar palette + the few custom colors into :root custom properties. Adopt Bootstrap's CSS variables (--bs-*) for everything else. Unblocks light/dark mode and any future rebrand.
  4. Pagination + filter component. EventLogs / ParkedMessages / AuditLog / Deployments all roll their own. Extract one <PagedTable TItem> or at least a <Paginator> that takes (page, pageSize, total) and emits standard events.
  5. TimestampDisplay audit. Make sure every consumer goes through it; standardise on UTC display + tooltip with relative time. Eliminate inline .ToString("HH:mm:ss") calls.
  6. One reference page for list patterns. Use Sites.razor as the reference; add a comment at the top of it pointing future implementers at it (or extract its skeleton into a snippet under docs/).

Out of scope / decisions to defer

  • Whether to migrate any list page from table-only to card grid (most should, but each is a separate ticket).
  • Dark-mode / theming work.
  • A real dashboard (KPI page) replacement.
  • Replacing the SignalR debug-view streaming model.