# 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-mappings` — **Medium**
**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}/edit` — **High**
**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/connections` — **High** 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}/edit` — **Medium**
**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-keys` — **Medium**
**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}/edit` — **Low**
**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-label`s, 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.
