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"). *@
+
From
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()
{