Make the seven sidebar section groups (Admin, Design, Deployment,
Notifications, Site Calls, Monitoring, Audit) collapsible. New NavSection
component renders a header toggle button (chevron) and reveals its items
only while expanded; NavMenu owns the expanded-section set.
Behaviour: sections are collapsed by default; state persists in the
`scadabridge_nav` cookie (written/read via the new nav-state.js JS interop,
mirroring treeview-storage.js) so it survives reloads and reconnects;
navigating into a section auto-expands it and remembers it. The Dashboard
item stays sectionless and always visible.
Tests: NavMenu bUnit tests expand sections before asserting items and add
collapsed-by-default / toggle / cookie-persistence cases; Playwright nav
tests expand sections before clicking links; new NavCollapseTests covers
the feature E2E. Build 0 warnings; bUnit 545 passed; Playwright nav suite
green (the unrelated AuditGridColumnTests resize-reload case remains
pre-existing flaky — an un-awaited save race in that test).
Adds drag-to-resize and drag-to-reorder column UX to AuditResultsGrid,
with chosen widths + column order persisted in browser sessionStorage.
- wwwroot/js/audit-grid.js: dependency-free helper — pointer-driven
resize handles, native HTML5 drag-and-drop reorder, and a
sessionStorage save/load wrapper (mirrors treeview-storage.js).
- AuditResultsGrid: renders a resize handle per <th>, makes headers
draggable, applies persisted widths via a --audit-col-width custom
property, and wires reorder into the existing ColumnOrder /
OrderedColumns() mechanism. JS-invokable OnColumnResized /
OnColumnReordered persist + re-render. A stored order naming an
unknown column degrades gracefully (drops unknown keys, appends
missing columns in default order); widths clamp to a 64px minimum.
- AuditResultsGrid.razor.css: subtle scoped styling for the resize
handle affordance and the reorder drop-target highlight.
- App.razor references audit-grid.js alongside the other scripts.
- Tests: 6 new bUnit tests for the load/apply/persist logic and
graceful degradation; a new AuditGridColumnTests Playwright suite
for the drag UX + reload persistence. Audit page bUnit tests set
loose JSInterop mode since the grid now calls into audit-grid.js.
Add a TlsMode read-only row and a None/StartTLS/SSL select to the SMTP
Configuration page edit form. New configs default to None; edits load
and persist the chosen mode through the repository.
Bundle G (#23 M7-T15): replace the temporary Admin-only gate on the Audit
Log surface with two new permission policies — OperationalAudit (read) and
AuditExport (bulk-export) — so the read path and the forensic-export path
can be delegated independently.
ScadaLink.Security
- AuthorizationPolicies: add OperationalAudit + AuditExport policy
constants; register them via RequireClaim with an explicit role allow-list
(OperationalAuditRoles, AuditExportRoles) so the role-to-permission
mapping is documented in one place.
- Default mapping: Admin and Audit roles grant both policies; AuditReadOnly
grants OperationalAudit only (read access without bulk export); Design
and Deployment grant neither.
ScadaLink.CentralUI
- AuditLogPage: switch the page-level [Authorize] to the OperationalAudit
policy and wrap the Export-CSV button in an AuthorizeView gated on
AuditExport so an OperationalAudit-only operator still sees the page +
filters but cannot trigger the CSV pull.
- ConfigurationAuditLog: switch from RequireAdmin to OperationalAudit so
both pages under the Audit nav group share the same gate.
- NavMenu: the Audit nav group now gates on OperationalAudit so the
section header + both child links match the per-page policies.
- AuditExportEndpoints: switch RequireAuthorization from RequireAdmin to
AuditExport — this is the authoritative gate; the AuthorizeView on the
button is just a UX affordance.
Tests
- New AuditLogPagePermissionTests covers the 5 brief-mandated cases plus
defence-in-depth for Admin-alone and AuditReadOnly users on the endpoint.
- SecurityTests: add policy-level coverage for the new role→permission
matrix (Theory rows pin every role/policy combination).
- AuditExportEndpointsTests: switch to AddScadaLinkAuthorization() so the
test host exercises the real production wiring under the new gate.
- AuditLogPageScaffoldTests: wrap the page render in a
CascadingAuthenticationState so the new in-page AuthorizeView resolves
the principal.
Adds three KPI tiles to the central Health dashboard for the Audit channel:
volume (rows in the last hour), error rate (Failed/Parked/Discarded over
total), and backlog (sum of SiteAuditBacklog.PendingCount across all sites).
Repo + service:
- IAuditLogRepository.GetKpiSnapshotAsync(window, nowUtc) — single aggregate
SELECT over the trailing window returning total + error counts; nowUtc is
optional for production callers and pinned by integration tests against the
shared MSSQL fixture so the global counts are deterministic.
- AuditLogQueryService.GetKpiSnapshotAsync() — composes the repo aggregate
with a sum of SiteAuditBacklog.PendingCount read from ICentralHealthAggregator.
- AuditLogKpiSnapshot record in Commons/Types/.
UI:
- New AuditKpiTiles Blazor component (Components/Health/) — three Bootstrap
card-tiles, click navigates to /audit/log with the matching pre-filter.
- Health.razor wires the tiles in alongside the existing Notification Outbox
KPIs; LoadAuditKpis() runs on every 10s refresh tick and degrades to em
dashes + inline error if the query fails.
- AuditLogPage extended to parse ?status= so the error-rate tile drill-in
(?status=Failed) auto-loads the grid.
Tests:
- AuditLogRepositoryTests: GetKpiSnapshotAsync mixed-status + empty-window
cases against the MSSQL migration fixture.
- AuditLogQueryServiceTests: forwarding + backlog composition; sites with
null SiteAuditBacklog contribute zero.
- AuditKpiTilesTests: 9 bUnit tests covering tile render, error-rate maths
with safe zero-events handling, em-dash unavailable path, click-through
navigation, and warning/danger border thresholds.
- HealthPageTests: new Renders_AuditKpiTiles_WithValues plus IAuditLogQueryService
stub registration in the constructor so existing outbox tests still pass.
- AuditLogPageScaffoldTests: ?status=Failed auto-load + unknown status drop.
Adds the results grid + query facade for the central Audit Log page
(#23 M7-T3):
* IAuditLogQueryService / AuditLogQueryService — CentralUI facade over
IAuditLogRepository.QueryAsync so the grid can be tested with a stubbed
query source. Default page size is 100; callers can override per call.
* AuditResultsGrid.razor + .razor.cs — Blazor Server component (Bootstrap
only, no third-party UI libs). Renders the 10 columns from
Component-AuditLog.md §10 (OccurredAtUtc, Site, Channel, Kind, Status,
Target, Actor, DurationMs, HttpStatus, ErrorMessage). Keyset-paged via
the last visible row's (OccurredAtUtc, EventId) as the cursor; Next-page
button disabled when the current page is short (no count query). Row
clicks emit OnRowSelected(AuditEvent) for Bundle C's drilldown drawer.
Status badges are colour-coded (Delivered=green; Failed/Parked/Discarded
=red; other=gray). Error messages truncated to 80 chars with full text
on hover.
* Column model framework: a ColumnOrder [Parameter] reorders columns by
stable string keys; unknown keys are dropped. M7 scope decision (in the
class doc): the framework is in place but drag-reorder / resize UX is
not implemented — M7.x can add persisted-per-user reordering without
rewriting the column model.
* AuditLogPage wired: hosts AuditFilterBar + AuditResultsGrid, threads
the filter through and stubs OnRowSelected for Bundle C.
* AuditLogQueryService registered as scoped in AddCentralUI.
* Tests: 4 grid bUnit tests (10 columns rendered, next-page cursor
carries last row, row click raises callback, badge classes for
Failed vs Delivered), 2 service tests (filter+paging pass-through,
default page size of 100). AuditLogPageScaffoldTests updated to
provide the new ISiteRepository + IAuditLogQueryService stubs the
page now resolves.
Adds the central-side Audit Log page scaffold at /audit/log (M7-T1) and
introduces a new Audit nav group (M7-T9) that hosts both the new page and
the renamed Configuration Audit Log. The page body is intentionally a
heading + two placeholders — Bundle B will land the AuditFilterBar and
AuditResultsGrid behind them.
The Audit nav group sits between Monitoring and the per-user footer; both
items inside are Admin-only, so the section header lives inside the
RequireAdmin AuthorizeView (non-admins see no orphan header).
bUnit scaffold tests pin the page heading, the section header order, and
the two child links; the existing 338 CentralUI tests continue to pass.