diff --git a/src/ScadaLink.Commons/Interfaces/Services/INodeIdentityProvider.cs b/src/ScadaLink.Commons/Interfaces/Services/INodeIdentityProvider.cs
new file mode 100644
index 0000000..f48fe25
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Services/INodeIdentityProvider.cs
@@ -0,0 +1,22 @@
+namespace ScadaLink.Commons.Interfaces.Services;
+
+///
+/// Surfaces the local node's semantic role-within-cluster name so downstream
+/// audit writers can stamp it on the SourceNode column.
+///
+///
+/// Conventional values follow the pattern node-a/node-b on site
+/// nodes and central-a/central-b on central nodes. The value is
+/// a free-form operator-supplied label — there is no enforced format. When the
+/// configuration value is missing, empty, or whitespace, implementations
+/// return null so audit writers can persist NULL rather than an empty
+/// string.
+///
+public interface INodeIdentityProvider
+{
+ ///
+ /// The configured semantic node name, trimmed of surrounding whitespace.
+ /// null when unconfigured.
+ ///
+ string? NodeName { get; }
+}
diff --git a/src/ScadaLink.Host/NodeIdentityProvider.cs b/src/ScadaLink.Host/NodeIdentityProvider.cs
new file mode 100644
index 0000000..6ea5fd9
--- /dev/null
+++ b/src/ScadaLink.Host/NodeIdentityProvider.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.Options;
+using ScadaLink.Commons.Interfaces.Services;
+
+namespace ScadaLink.Host;
+
+///
+/// Binds to .
+/// Empty or whitespace values are normalised to null; otherwise the value
+/// is returned trimmed.
+///
+internal sealed class NodeIdentityProvider : INodeIdentityProvider
+{
+ public string? NodeName { get; }
+
+ public NodeIdentityProvider(IOptions nodeOptions)
+ {
+ var configured = nodeOptions.Value.NodeName;
+ NodeName = string.IsNullOrWhiteSpace(configured) ? null : configured.Trim();
+ }
+}
diff --git a/src/ScadaLink.Host/NodeOptions.cs b/src/ScadaLink.Host/NodeOptions.cs
index 3bc85fb..4088cb5 100644
--- a/src/ScadaLink.Host/NodeOptions.cs
+++ b/src/ScadaLink.Host/NodeOptions.cs
@@ -4,6 +4,14 @@ public class NodeOptions
{
public string Role { get; set; } = string.Empty;
public string NodeHostname { get; set; } = string.Empty;
+
+ ///
+ /// Operator-configured semantic node name used to stamp the SourceNode
+ /// column on audit rows. Conventional values are node-a/node-b
+ /// on site nodes and central-a/central-b on central nodes,
+ /// but the value is a free-form label — no validation is enforced.
+ ///
+ public string NodeName { get; set; } = string.Empty;
public string? SiteId { get; set; }
public int RemotingPort { get; set; } = 8081;
public int GrpcPort { get; set; } = 8083;
diff --git a/src/ScadaLink.Host/ScadaLink.Host.csproj b/src/ScadaLink.Host/ScadaLink.Host.csproj
index 0f027e6..8bb5a4b 100644
--- a/src/ScadaLink.Host/ScadaLink.Host.csproj
+++ b/src/ScadaLink.Host/ScadaLink.Host.csproj
@@ -7,6 +7,10 @@
true
+
+
+
+
diff --git a/src/ScadaLink.Host/SiteServiceRegistration.cs b/src/ScadaLink.Host/SiteServiceRegistration.cs
index 0e59f9b..97ce314 100644
--- a/src/ScadaLink.Host/SiteServiceRegistration.cs
+++ b/src/ScadaLink.Host/SiteServiceRegistration.cs
@@ -1,6 +1,7 @@
using ScadaLink.AuditLog;
using ScadaLink.ClusterInfrastructure;
using ScadaLink.Communication;
+using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.DataConnectionLayer;
using ScadaLink.ExternalSystemGateway;
using ScadaLink.HealthMonitoring;
@@ -96,5 +97,10 @@ public static class SiteServiceRegistration
services.Configure(config.GetSection("ScadaLink:HealthMonitoring"));
services.Configure(config.GetSection("ScadaLink:Notification"));
services.Configure(config.GetSection("ScadaLink:Logging"));
+
+ // Audit Log (#23) — exposes ScadaLink:Node:NodeName to downstream audit
+ // writers so they can stamp the SourceNode column. Registered here in
+ // shared bootstrap because every node (central + site) needs it.
+ services.AddSingleton();
}
}
diff --git a/tests/ScadaLink.Host.Tests/NodeIdentityProviderTests.cs b/tests/ScadaLink.Host.Tests/NodeIdentityProviderTests.cs
new file mode 100644
index 0000000..c7322a2
--- /dev/null
+++ b/tests/ScadaLink.Host.Tests/NodeIdentityProviderTests.cs
@@ -0,0 +1,50 @@
+using Microsoft.Extensions.Options;
+using ScadaLink.Commons.Interfaces.Services;
+
+namespace ScadaLink.Host.Tests;
+
+///
+/// Tests for NodeIdentityProvider — surfaces the operator-configured semantic
+/// node name (e.g. node-a / node-b / central-a / central-b) used by downstream
+/// audit writers to stamp the SourceNode column.
+///
+public class NodeIdentityProviderTests
+{
+ private static INodeIdentityProvider BuildProvider(string nodeName)
+ {
+ var options = Options.Create(new NodeOptions { NodeName = nodeName });
+ return new NodeIdentityProvider(options);
+ }
+
+ [Fact]
+ public void NodeIdentityProvider_returns_configured_NodeName()
+ {
+ var provider = BuildProvider("central-a");
+
+ Assert.Equal("central-a", provider.NodeName);
+ }
+
+ [Fact]
+ public void NodeIdentityProvider_returns_null_when_NodeName_unset()
+ {
+ var provider = BuildProvider(string.Empty);
+
+ Assert.Null(provider.NodeName);
+ }
+
+ [Fact]
+ public void NodeIdentityProvider_returns_null_when_NodeName_whitespace()
+ {
+ var provider = BuildProvider(" ");
+
+ Assert.Null(provider.NodeName);
+ }
+
+ [Fact]
+ public void NodeIdentityProvider_trims_whitespace()
+ {
+ var provider = BuildProvider(" node-a ");
+
+ Assert.Equal("node-a", provider.NodeName);
+ }
+}