diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/ActiveNodeHealthCheck.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/ActiveNodeHealthCheck.cs
new file mode 100644
index 0000000..97c1801
--- /dev/null
+++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/ActiveNodeHealthCheck.cs
@@ -0,0 +1,138 @@
+using Akka.Actor;
+using Akka.Cluster;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace ZB.MOM.WW.Health.Akka;
+
+///
+/// Pure decision function for the active / leader probe, factored out of
+/// so the role-less and role-filtered matrices are exhaustively
+/// table-testable without forming a real cluster.
+///
+public static class ActiveNodeDecision
+{
+ ///
+ /// Maps the resolved cluster facts to a .
+ ///
+ /// Whether the local node's member status is Up.
+ ///
+ /// Whether the local node is the leader: the cluster leader in role-less mode, or the
+ /// role-singleton leader in role-filtered mode.
+ ///
+ ///
+ /// Whether the local node carries . Ignored when
+ /// is null.
+ ///
+ ///
+ /// The role to scope the check to, or null for the role-less (whole-cluster-leader) mode.
+ ///
+ ///
+ /// Role-less: Healthy iff the node is Up and the cluster leader, otherwise Unhealthy.
+ /// Role-filtered: Healthy when the node lacks the role (probe irrelevant) or carries the role and
+ /// is the role-singleton leader; Degraded when it carries the role but is not the leader.
+ ///
+ public static HealthStatus Evaluate(bool selfUp, bool isLeader, bool hasRole, string? requiredRole)
+ {
+ if (requiredRole is null)
+ return selfUp && isLeader ? HealthStatus.Healthy : HealthStatus.Unhealthy;
+
+ if (!hasRole)
+ return HealthStatus.Healthy;
+
+ return isLeader ? HealthStatus.Healthy : HealthStatus.Degraded;
+ }
+}
+
+///
+/// Health check that reports whether this node is the designated active / leader node.
+/// An optional role scopes the check to nodes carrying that role. Register to the
+/// tag.
+///
+///
+/// The is resolved lazily from the service provider. If it is not yet
+/// available — e.g. during startup before Akka is initialised — the check returns
+/// rather than throwing, so it is startup-safe.
+///
+public sealed class ActiveNodeHealthCheck : IHealthCheck
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly string? _role;
+
+ ///
+ /// Role-less constructor: Healthy when the node is Up and the cluster leader
+ /// (ScadaBridge ActiveNode pattern); Unhealthy otherwise. Degraded when the ActorSystem /
+ /// cluster is not yet ready.
+ ///
+ ///
+ /// The application service provider. The is resolved lazily so the
+ /// check is startup-safe: if no is registered yet the result is Degraded.
+ ///
+ public ActiveNodeHealthCheck(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ _role = null;
+ }
+
+ ///
+ /// Role-filtered constructor: Healthy when the node lacks or carries it
+ /// and is the role-singleton leader; Degraded when it carries the role but is not the leader
+ /// (OtOpcUa AdminRoleLeader pattern). Degraded when the ActorSystem / cluster is not yet ready.
+ ///
+ ///
+ /// The application service provider. The is resolved lazily so the
+ /// check is startup-safe: if no is registered yet the result is Degraded.
+ ///
+ /// The Akka cluster role to scope the check to.
+ public ActiveNodeHealthCheck(IServiceProvider serviceProvider, string role)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ _role = role ?? throw new ArgumentNullException(nameof(role));
+ }
+
+ ///
+ public Task CheckHealthAsync(
+ HealthCheckContext context,
+ CancellationToken cancellationToken = default)
+ {
+ var system = _serviceProvider.GetService();
+ if (system is null)
+ return Task.FromResult(HealthCheckResult.Degraded("ActorSystem not yet available."));
+
+ var cluster = Cluster.Get(system);
+ var self = cluster.SelfMember;
+ var selfUp = self.Status == MemberStatus.Up;
+
+ bool hasRole;
+ bool isLeader;
+ if (_role is null)
+ {
+ hasRole = false;
+ var leader = cluster.State.Leader;
+ isLeader = leader is not null && leader == self.Address;
+ }
+ else
+ {
+ hasRole = self.HasRole(_role);
+ var roleLeader = cluster.State.RoleLeader(_role);
+ isLeader = roleLeader is not null && roleLeader == self.Address;
+ }
+
+ var health = ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, _role);
+ return Task.FromResult(new HealthCheckResult(health, DescribeResult(health, self.Status)));
+ }
+
+ private string DescribeResult(HealthStatus health, MemberStatus status)
+ {
+ if (_role is null)
+ return health == HealthStatus.Healthy
+ ? "Active node (cluster leader)."
+ : $"Standby node (status: {status}).";
+
+ return health switch
+ {
+ HealthStatus.Healthy => $"Active for role '{_role}' (or not a role member).",
+ _ => $"Role '{_role}' member but not leader.",
+ };
+ }
+}
diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/AkkaActiveNodeGate.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/AkkaActiveNodeGate.cs
new file mode 100644
index 0000000..6a55fea
--- /dev/null
+++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.Akka/AkkaActiveNodeGate.cs
@@ -0,0 +1,50 @@
+using Akka.Actor;
+using Akka.Cluster;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ZB.MOM.WW.Health.Akka;
+
+///
+/// implementation that computes directly
+/// from the Akka cluster state (self member Up and the local node is the cluster leader).
+/// Register as a singleton.
+///
+///
+/// The is resolved lazily from the service provider; if it is not yet
+/// available — e.g. during startup before Akka is initialised — returns
+/// false (the safe default during startup). This gate reads the cluster state directly and
+/// does not resolve from DI.
+///
+public sealed class AkkaActiveNodeGate : IActiveNodeGate
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ /// Initializes a new .
+ ///
+ /// The application service provider. The is resolved lazily; if it is
+ /// not yet available returns false.
+ ///
+ public AkkaActiveNodeGate(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ }
+
+ ///
+ public bool IsActiveNode
+ {
+ get
+ {
+ var system = _serviceProvider.GetService();
+ if (system is null)
+ return false;
+
+ var cluster = Cluster.Get(system);
+ var self = cluster.SelfMember;
+ if (self.Status != MemberStatus.Up)
+ return false;
+
+ var leader = cluster.State.Leader;
+ return leader is not null && leader == self.Address;
+ }
+ }
+}
diff --git a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Akka.Tests/ActiveNodeDecisionTests.cs b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Akka.Tests/ActiveNodeDecisionTests.cs
new file mode 100644
index 0000000..1296376
--- /dev/null
+++ b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Akka.Tests/ActiveNodeDecisionTests.cs
@@ -0,0 +1,91 @@
+using Akka.Actor;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using ZB.MOM.WW.Health.Akka;
+
+namespace ZB.MOM.WW.Health.Akka.Tests;
+
+///
+/// Table-driven tests for the pure helper covering both
+/// the role-less (ScadaBridge ActiveNode) and role-filtered (OtOpcUa AdminRoleLeader) matrices,
+/// plus the startup-safety null-guards on and
+/// when no is registered.
+///
+public sealed class ActiveNodeDecisionTests
+{
+ // Role-less: requiredRole == null. hasRole is irrelevant. Healthy iff (selfUp && isLeader), else Unhealthy.
+ public static IEnumerable