Commit Graph

141 Commits

Author SHA1 Message Date
Joseph Doherty f973f49254 fix(ui): remove LDAP credentials tagline from Login page 2026-05-23 18:56:24 -04:00
Joseph Doherty c754666a3d fix(ui): carry SourceNode on SiteCallDetail + NotificationDetail records
The Site Calls and Notifications detail modals were reading SourceNode from
the summary record (d.SourceNode) while every other field read from the
detail record (det.X). The pattern works today because the modal always
opens via a row click that pre-loads the summary, but a future drill-in
from a deep link or refresh path could leave the summary stale or null and
the field would render blank or wrong.

Add SourceNode to both detail records, project it through the actor's
ToDetail mapping, and switch the razor markup to read det.SourceNode. Now
the modal binds uniformly to the detail record across all fields.
2026-05-23 18:37:53 -04:00
Joseph Doherty d18a6e6fa0 feat(ui): add Node column + filter to SiteCalls grid 2026-05-23 18:08:25 -04:00
Joseph Doherty b9c017136d feat(ui): add Node column + filter to NotificationOutbox grid 2026-05-23 18:04:59 -04:00
Joseph Doherty b07f43a308 feat(centralui): rebrand web UI to ScadaBridge + technical-light theme
Rename the user-facing product name from ScadaLink to ScadaBridge across
the six display strings (browser title, sidebar brand, login + not-authorized
headings, dashboard welcome/subtitle). Namespaces, assemblies, config keys,
and _content/ScadaLink.CentralUI asset routes are unchanged.

Apply the technical-light design system: vendor theme.css + IBM Plex fonts
into the CentralUI RCL, include theme.css globally (after Bootstrap so its
--bs-* token overrides win), and restyle the layout chrome to a light
sidebar — white surface, hairline rules, ink text, accent-blue active item,
the brand accent mark. Page markup stays Bootstrap and inherits the warm
paper background, Plex type, accent, and hairline borders via the tokens.

Tests: build 0 warnings; bUnit 542 passed; Playwright 64 passed.
2026-05-22 07:03:46 -04:00
Joseph Doherty 3f1ad08f42 feat(centralui): open ExecutionDetailModal on tree-node double-click 2026-05-22 01:46:12 -04:00
Joseph Doherty afd81c32ef fix(centralui): marshal Audit Log LocationChanged handler through InvokeAsync
Code-review follow-ups on the same-page drill-in fix (3f1c0e5):
- Wrap HandleLocationChanged's body in InvokeAsync — LocationChanged can
  fire off the renderer's synchronization context.
- Document that a paramless /audit/log navigation intentionally preserves
  the last applied filter (drill-ins always carry query params).
2026-05-21 20:35:20 -04:00
Joseph Doherty 3f1c0e5018 fix(centralui): re-apply Audit Log query-string filters on same-page drill-in
The drilldown drawer's 'View this/parent execution' actions call
NavigationManager.NavigateTo('/audit/log?executionId=...') while the
user is already on the routed AuditLogPage. Blazor treats this as a
same-component navigation, so OnInitialized does not re-run and
ApplyQueryStringFilters() (which was wired only to OnInitialized) never
re-parsed the new query string: _currentFilter stayed stale and the
results grid never reloaded to the drill-in target.

AuditLogPage now subscribes to NavigationManager.LocationChanged,
re-applies the query-string filters on every location change (closing
the drawer and calling StateHasChanged), and unsubscribes via
IDisposable. The 'View parent execution' drill-in now genuinely lands
on /audit/log?executionId={parentId} with the grid reloaded.

Also corrects the Playwright test wait: a same-page query-string Blazor
navigation pushes history.pushState over the SignalR circuit rather
than triggering a document load, so WaitForLoadState(NetworkIdle)
returned before the URL settled. Switched to WaitForURLAsync, the
correct primitive for SPA/pushState navigations.
2026-05-21 20:30:48 -04:00
Joseph Doherty 34a4356625 feat(centralui): execution-chain tree view on the Audit Log page 2026-05-21 18:49:13 -04:00
Joseph Doherty 0b5723b777 feat(centralui): ParentExecutionId column, filter and parent drill-in on the Audit Log page 2026-05-21 18:38:02 -04:00
Joseph Doherty 1ba62052d6 feat(centralui): ExecutionId column, filter and drill-in on the Audit Log page 2026-05-21 15:52:57 -04:00
Joseph Doherty f64a7aed02 refactor(audit): consolidate query-param parsers; widen CLI export to multi-value 2026-05-21 05:37:06 -04:00
Joseph Doherty 2a76be1f94 feat(audit): multi-value filters across ManagementService, CLI and Central UI 2026-05-21 05:27:17 -04:00
Joseph Doherty 37c7a0e5ac feat(auditlog): multi-value AuditLogQueryFilter dimensions 2026-05-21 05:15:51 -04:00
Joseph Doherty b3b02a8cb6 fix(centralui): apply status/stuck query-string filters on the Site Calls page 2026-05-21 05:08:50 -04:00
Joseph Doherty 44f1ee372a feat(centralui): Site Call KPI tiles on the Health dashboard 2026-05-21 05:04:16 -04:00
Joseph Doherty d73b459057 fix(centralui): single relay toast, paging/skip polish, extra Site Calls tests 2026-05-21 04:59:12 -04:00
Joseph Doherty 7e9d74697b feat(centralui): Site Calls page with Retry/Discard and Audit drill-in 2026-05-21 04:51:14 -04:00
Joseph Doherty babf5b99e7 feat(ui): notification detail modal shows message body + recipients 2026-05-21 02:49:17 -04:00
Joseph Doherty ef5cf76026 feat(ui): notification report row double-click opens detail modal 2026-05-21 02:39:41 -04:00
Joseph Doherty c66ef71017 feat(ui): SMTP config form TlsMode field
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.
2026-05-21 02:13:02 -04:00
Joseph Doherty 6dea84cd28 feat(security): OperationalAudit + AuditExport permissions for Audit Log surface (#23 M7)
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.
2026-05-20 21:09:42 -04:00
Joseph Doherty 8744630adb feat(ui): server-side streaming CSV export of Audit Log (#23 M7) 2026-05-20 20:57:01 -04:00
Joseph Doherty 943c2ced39 feat(ui): Audit KPI tiles on Health dashboard (#23 M7)
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.
2026-05-20 20:43:57 -04:00
Joseph Doherty 38fc9b4102 feat(ui): drill-ins from detail pages to Audit Log (#23 M7)
Adds "Recent audit activity" deep links from four edit/detail pages into
the central Audit Log, each with a pre-filter encoded in the query string
that the Audit Log page (Bundle D0) now parses on initialization:

  - External Systems (Design/ExternalSystemForm)      → ?target={Name}
  - API Keys         (Admin/ApiKeyForm)                → ?actor={Name}&channel=ApiInbound
  - Sites            (Admin/SiteForm)                  → ?site={SiteIdentifier}
  - Instances        (Deployment/InstanceConfigure)    → ?instance={UniqueName}

The link is suppressed on create/new flows where there is nothing to
drill into yet. Instance is UI-only on the filter bar (the repository
filter contract has no instance column), so the page-side prefill threads
through the InitialInstanceSearch seam on AuditFilterBar.

Site Calls (#22 M7-T11) drill-in is DEFERRED: the Central UI does not
yet host a Site Calls listing page, per M3 reality notes. Add the
drill-in when that page lands.

#23 M7-T12
2026-05-20 20:26:28 -04:00
Joseph Doherty 1c20e81d77 feat(ui): drill-in from Notifications to Audit Log (#23 M7) 2026-05-20 20:20:54 -04:00
Joseph Doherty 450f8bca28 feat(ui): AuditLogPage parses query-string filters for drill-ins (#23 M7) 2026-05-20 20:19:47 -04:00
Joseph Doherty ae4480e7aa feat(ui): AuditDrilldownDrawer with JSON/SQL render, cURL, drill-back, redaction badges (#23 M7)
Implements Bundle C (M7-T4 through M7-T8) of the Audit Log #23 M7
Central UI work: a right-side off-canvas drawer that opens from
AuditResultsGrid row clicks and renders one AuditEvent in full.

Cohesive single-component delivery:
- Read-only fields stacked (form-layout memory): Channel/Kind, Status,
  HttpStatus, Target, Actor, Source* provenance, CorrelationId,
  OccurredAtUtc, IngestedAtUtc, DurationMs.
- Channel-aware body renderer: DbOutbound {sql, parameters} payloads
  render a code-block with CSS-only .language-sql class plus a
  parameter <dl>; other channels JSON-pretty-print when parseable and
  fall back to verbatim <pre>.
- Redaction badges on Request/Response when the body contains the
  <redacted> or <redacted: redactor error> sentinels.
- Copy-as-cURL (API channels only) builds a curl command from Target
  + optional {method, headers, body} RequestSummary JSON and writes
  it via navigator.clipboard.writeText.
- Show-all-events drill-back navigates to /audit/log?correlationId={id}
  when the event carries a CorrelationId.
- Close button + backdrop-click both raise OnClose.

AuditLogPage wires Event/IsOpen/OnClose; row clicks now open the
drawer (HandleRowSelected pins _selectedEvent + _drawerOpen=true).

11 bUnit tests cover field rendering, JSON pretty-print, verbatim
fallback, SQL block, conditional buttons, redaction badges,
navigation drill-back, and clipboard interop. No third-party UI
libraries: Bootstrap offcanvas + scoped razor.css only.
2026-05-20 20:13:33 -04:00
Joseph Doherty e052aa4ff8 feat(ui): AuditResultsGrid + AuditLogQueryService with keyset paging (#23 M7)
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.
2026-05-20 20:02:46 -04:00
Joseph Doherty 12b86bea7a feat(ui): scaffold Audit Log page + Audit nav group (#23 M7)
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.
2026-05-20 19:49:11 -04:00
Joseph Doherty a9f45b0861 refactor(ui): rename AuditLog viewer to ConfigurationAuditLog under /audit/configuration (#23 M7)
The pre-M1 IAuditService config-change viewer moves out of the Monitoring
nav group to make room for the new Audit nav group (issue #23 M7). The
old route /monitoring/audit-log returns 404 (no redirect, per plan) — the
viewer is now reachable at /audit/configuration and labelled
"Configuration Audit Log" to disambiguate from the new Audit Log page
(arriving in #23 M7-T9). Inbound references in NavMenu, Dashboard, and
the Playwright nav tests are updated to the new route and label.
2026-05-20 19:46:09 -04:00
Joseph Doherty 547a7b98e5 fix(central-ui): load notification-list recipients sequentially to avoid concurrent DbContext use 2026-05-19 06:34:32 -04:00
Joseph Doherty 82745ef916 feat(central-ui): link Health outbox tiles to the Notification KPIs page 2026-05-19 06:22:02 -04:00
Joseph Doherty 22bac058dd feat(central-ui): Notification KPIs page with per-site breakdown 2026-05-19 06:09:43 -04:00
Joseph Doherty 34e464edab refactor(central-ui): split Notification Report out of the Outbox page 2026-05-19 06:03:15 -04:00
Joseph Doherty 016f5d48a6 refactor(central-ui): co-locate NotificationListForm with the Notifications pages 2026-05-19 05:59:40 -04:00
Joseph Doherty 36332e5a94 refactor(central-ui): move Notification List form to /notifications, drop External Systems tab 2026-05-19 05:56:45 -04:00
Joseph Doherty 0fa4ac5525 refactor(central-ui): contextual errors, parallel recipient load, delete-path test for Notification Lists 2026-05-19 05:54:48 -04:00
Joseph Doherty 0f90c0ad9c feat(central-ui): standalone Notification Lists page 2026-05-19 05:49:45 -04:00
Joseph Doherty 25beb833fc refactor(central-ui): move SMTP Configuration page to /notifications/smtp 2026-05-19 05:45:06 -04:00
Joseph Doherty af22aa7ce1 refactor(notification-outbox): clarify Health outbox-KPI seed value 2026-05-19 03:09:44 -04:00
Joseph Doherty 9e7bc7b541 feat(notification-outbox): add outbox KPI tiles to Health dashboard 2026-05-19 03:05:41 -04:00
Joseph Doherty 9b05e48ea6 test(notification-outbox): cover Discard and query-failure paths on the Outbox page 2026-05-19 03:02:48 -04:00
Joseph Doherty ad9872705d feat(notification-outbox): add Notification Outbox UI page 2026-05-19 02:58:49 -04:00
Joseph Doherty 381eea63b1 refactor(central-ui): drop redundant Parent Template field from Template Properties
The Template Properties card repeated the parent template, which the page
header already shows — the "inherits X" line for base templates and the
"Derived from X — composed inside Y" line for derived ones. The card now
carries only Name and Description.
2026-05-18 19:14:09 -04:00
Joseph Doherty 06462a0100 feat(template-engine): contained names for composition-derived templates
A composition-derived template now stores its contained name — the
composition slot's InstanceName (e.g. "Pump"), unique only within its
owner — instead of the dotted global path ("Motor Controller.Pump").
The qualified hierarchical name is computed on read.

- TemplateNaming.QualifiedName: walks the OwnerCompositionId chain to
  build the dotted path; null-safe, cycle-guarded.
- TemplateConfiguration: the unique index on Template.Name becomes
  filtered (WHERE IsDerived = 0) — base templates stay globally unique;
  derived templates' uniqueness is the existing (TemplateId,
  InstanceName) index on TemplateComposition.
- Migration ContainedDerivedTemplateNames: rewrites derived rows to the
  contained name; Down rebuilds the dotted names via a recursive CTE
  before restoring the global index.
- TemplateService: composition create/rename store the contained name;
  the dotted-name collision pre-checks and cascade-rename are removed
  (a slot rename no longer touches nested derived templates).
- TemplateEdit: title shows the contained name; the qualified path is a
  breadcrumb subtitle; "composed inside" uses the owner's qualified name.

TDD: 4 TemplateNaming tests + updated composition tests. TemplateEngine
293, ConfigurationDatabase 114, CentralUI 316 green. Migration applied to
the dev cluster and verified in the browser (Motor Controller.Pump now
titled "Pump"; nested Motor Controller.Pump.TempSensor resolves).

Design: docs/plans/2026-05-18-contained-template-names-design.md
2026-05-18 17:50:30 -04:00
Joseph Doherty 36c6036060 feat(central-ui): enlarge script modal; tab the Shared Script form
Script editor modal (TemplateEdit): the tabbed Trigger/Code/Parameters/
Return content is substantial, so the dialog now fills most of the
viewport — a .script-editor-modal class (96vw wide, ~full height) replaces
modal-xl, paired with modal-dialog-scrollable so the body scrolls.

Shared Script create/edit form (SharedScriptForm): Code, Parameters, and
Return type move from stacked sections into a tab strip, matching the
template script modal. Panels toggle via display:none so the Monaco editor
and JSONJoy island stay mounted across tab switches; Code is the default
tab. Name stays above the tabs.

Markup/CSS only — no logic change. CentralUI suite 316 green; both
verified in the browser.
2026-05-18 17:06:28 -04:00
Joseph Doherty e1a4ce4de8 refactor(central-ui): move script Trigger section into the tabbed panel
The Add/Edit Script modal's Trigger configuration (trigger editor + Min
time between runs) moves out of the always-visible header area and into
the tab strip as a new first tab: Trigger | Code | Parameters | Return
type. Trigger is the default selected tab.

Name and Locked remain above the tabs. The Trigger panel toggles via
display:none like the others, so the trigger expression's Monaco editor
stays mounted across tab switches. Markup-only — no logic change; verified
in the browser. CentralUI suite 316 green.
2026-05-18 16:51:58 -04:00
Joseph Doherty 01509a045f feat(central-ui): add Min time between runs field to the script form
The template script editor had no input for MinTimeBetweenRuns, so a
WhileTrue trigger configured through the UI always saved a null interval
and degraded to a single edge fire. The Add/Edit Script modal now has a
"Min time between runs" number+unit (ms/sec/min) field.

- Visible only for ValueChange / Conditional / Expression triggers — the
  auto-firing triggers MinTimeBetweenRuns throttles. Hidden for Interval
  (its own period is the cadence), Call (invoked explicitly, never
  throttled), and None.
- For a WhileTrue Conditional/Expression trigger the field is labelled as
  the re-fire interval and shows a warning while it is blank.
- Wired through the new-script and edit-script save paths (edit previously
  only preserved the existing value, never let the user change it).

New DurationInput helper does the TimeSpan <-> number+unit conversion;
ScriptTriggerConfigCodec.SupportsMinTimeBetweenRuns classifies trigger
types. Both TDD'd — 21 new tests. CentralUI suite 316 green; verified
end-to-end in the browser (visibility per trigger type, WhileTrue warning,
save/reload round-trip).
2026-05-18 16:44:15 -04:00
Joseph Doherty e55bd46ca1 fix(health-monitoring): resolve HealthMonitoring-015 — nullable LastReportReceivedAt
A heartbeat-registered site that has never sent a full report now has
LastReportReceivedAt = null instead of the year-0001 sentinel. TimestampDisplay
accepts DateTimeOffset? and renders null as a placeholder ('awaiting first
report') rather than a ~2000-year-stale date. Cross-module: HealthMonitoring +
CentralUI.
2026-05-17 05:43:05 -04:00