diff --git a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor index 30ece7b..511c24c 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor @@ -58,6 +58,17 @@ } + @* Task 17: free-text Node filter — exact match against the + SiteCall.SourceNode column. The Source site dropdown narrows + to a site; Node narrows further within that site (or across + sites if Source site is "Any"). *@ +
+ + +
Tracked operation Source site + Node Channel Target Status @@ -143,6 +155,7 @@ title="Double-click for full detail"> @ShortId(c.TrackedOperationId) @SiteName(c.SourceSite) + @(c.SourceNode ?? "—") @c.Channel @c.Target @@ -253,6 +266,9 @@
Source site
@SiteName(det.SourceSite)
+
Source node
+
@(string.IsNullOrEmpty(d.SourceNode) ? "—" : d.SourceNode)
+
Channel
@det.Channel
diff --git a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs index c8f0a21..726a025 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs +++ b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs @@ -82,6 +82,7 @@ public partial class SiteCallsReport private string _channelFilter = string.Empty; private string _siteFilter = string.Empty; private string _targetFilter = string.Empty; + private string _nodeFilter = string.Empty; private bool _stuckOnly; private DateTime? _fromFilter; private DateTime? _toFilter; @@ -204,7 +205,8 @@ public partial class SiteCallsReport ToUtc: ToUtc(_toFilter), AfterCreatedAtUtc: cursor.AfterCreatedAtUtc, AfterId: cursor.AfterId, - PageSize: PageSize); + PageSize: PageSize, + SourceNodeFilter: NullIfEmpty(_nodeFilter)); var response = await CommunicationService.QuerySiteCallsAsync(request); if (response.Success) @@ -393,6 +395,7 @@ public partial class SiteCallsReport _channelFilter = string.Empty; _siteFilter = string.Empty; _targetFilter = string.Empty; + _nodeFilter = string.Empty; _stuckOnly = false; _fromFilter = null; _toFilter = null; @@ -403,6 +406,7 @@ public partial class SiteCallsReport !string.IsNullOrEmpty(_channelFilter) || !string.IsNullOrEmpty(_siteFilter) || !string.IsNullOrEmpty(_targetFilter) || + !string.IsNullOrEmpty(_nodeFilter) || _stuckOnly || _fromFilter != null || _toFilter != null; diff --git a/src/ScadaLink.Commons/Messages/Audit/SiteCallQueries.cs b/src/ScadaLink.Commons/Messages/Audit/SiteCallQueries.cs index d5c98a4..93658c5 100644 --- a/src/ScadaLink.Commons/Messages/Audit/SiteCallQueries.cs +++ b/src/ScadaLink.Commons/Messages/Audit/SiteCallQueries.cs @@ -33,7 +33,8 @@ public sealed record SiteCallQueryRequest( DateTime? ToUtc, DateTime? AfterCreatedAtUtc, Guid? AfterId, - int PageSize); + int PageSize, + string? SourceNodeFilter = null); /// /// A single SiteCalls row summarised for the Site Calls UI grid. Carries @@ -61,7 +62,8 @@ public sealed record SiteCallSummary( DateTime CreatedAtUtc, DateTime UpdatedAtUtc, DateTime? TerminalAtUtc, - bool IsStuck); + bool IsStuck, + string? SourceNode = null); /// /// Central -> Site Calls UI: paginated response for a . diff --git a/src/ScadaLink.Commons/Types/Audit/SiteCallQueryFilter.cs b/src/ScadaLink.Commons/Types/Audit/SiteCallQueryFilter.cs index 63f0c58..3624191 100644 --- a/src/ScadaLink.Commons/Types/Audit/SiteCallQueryFilter.cs +++ b/src/ScadaLink.Commons/Types/Audit/SiteCallQueryFilter.cs @@ -26,6 +26,11 @@ namespace ScadaLink.Commons.Types.Audit; /// keeps the "StuckOnly" filter honest so paging never returns under-filled /// pages with a non-null next cursor. /// +/// +/// Restrict to cached calls originating at a specific cluster node (e.g. +/// "site-plant-a-node-a"). Exact match; null means "do not +/// constrain". Rows with NULL SourceNode are excluded when set. +/// public sealed record SiteCallQueryFilter( string? Channel = null, string? SourceSite = null, @@ -33,4 +38,5 @@ public sealed record SiteCallQueryFilter( string? Target = null, DateTime? FromUtc = null, DateTime? ToUtc = null, - DateTime? StuckCutoffUtc = null); + DateTime? StuckCutoffUtc = null, + string? SourceNode = null); diff --git a/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs b/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs index a06282f..ca3c226 100644 --- a/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs +++ b/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs @@ -204,6 +204,7 @@ SELECT TOP ({paging.PageSize}) FROM dbo.SiteCalls WHERE ({filter.Channel} IS NULL OR Channel = {filter.Channel}) AND ({filter.SourceSite} IS NULL OR SourceSite = {filter.SourceSite}) + AND ({filter.SourceNode} IS NULL OR SourceNode = {filter.SourceNode}) AND ({filter.Status} IS NULL OR Status = {filter.Status}) AND ({filter.Target} IS NULL OR Target = {filter.Target}) AND ({fromUtc} IS NULL OR CreatedAtUtc >= {fromUtc}) diff --git a/src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs b/src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs index 8078060..7539f3a 100644 --- a/src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs +++ b/src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs @@ -232,7 +232,8 @@ public class SiteCallAuditActor : ReceiveActor // TerminalAtUtc IS NULL AND CreatedAtUtc < cutoff composes with the // keyset cursor, so the page is always honest (full pages, no empty // pages with a non-null next cursor). - StuckCutoffUtc: request.StuckOnly ? stuckCutoff : null); + StuckCutoffUtc: request.StuckOnly ? stuckCutoff : null, + SourceNode: NullIfBlank(request.SourceNodeFilter)); var pageSize = Math.Clamp(request.PageSize, 1, MaxPageSize); var paging = new SiteCallPaging( @@ -633,7 +634,8 @@ public class SiteCallAuditActor : ReceiveActor CreatedAtUtc: row.CreatedAtUtc, UpdatedAtUtc: row.UpdatedAtUtc, TerminalAtUtc: row.TerminalAtUtc, - IsStuck: IsStuck(row, stuckCutoff)); + IsStuck: IsStuck(row, stuckCutoff), + SourceNode: row.SourceNode); } private static SiteCallDetail ToDetail(SiteCall row) diff --git a/tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs b/tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs index 42093c5..0d6fd26 100644 --- a/tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs +++ b/tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs @@ -223,6 +223,43 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture(TimeSpan.FromSeconds(10)); + Assert.True(response.Success); + Assert.Single(response.SiteCalls); + Assert.Equal("site-plant-a-node-a", response.SiteCalls[0].SourceNode); + Assert.Equal("Attempted", response.SiteCalls[0].Status); + } + [SkippableFact] public async Task SiteCallQueryRequest_KeysetPaging_AdvancesViaCursor() {