diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/FleetStatusSignalRBridge.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/FleetStatusSignalRBridge.cs
new file mode 100644
index 0000000..ea6c09c
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/FleetStatusSignalRBridge.cs
@@ -0,0 +1,52 @@
+using Akka.Actor;
+using Akka.Cluster.Tools.PublishSubscribe;
+using Akka.Event;
+using Microsoft.AspNetCore.SignalR;
+using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
+
+///
+/// Akka actor that subscribes to the fleet-status DistributedPubSub topic and forwards
+/// every snapshot to all SignalR clients connected to
+/// . Spawned on admin-role nodes by
+/// AddOtOpcUaSignalRBridges.
+///
+/// The bridge runs locally on each admin node — every node that hosts the hub also forwards
+/// snapshots to its own connected clients. That keeps the hub-to-actor wiring simple (no
+/// cluster-singleton coordination needed) at the cost of duplicated DPS subscriptions on
+/// multi-admin deployments. Acceptable since the messages are small + the SignalR fan-out
+/// is per-node anyway.
+///
+public sealed class FleetStatusSignalRBridge : ReceiveActor
+{
+ public const string TopicName = "fleet-status";
+
+ private readonly IHubContext _hub;
+ private readonly ILoggingAdapter _log = Context.GetLogger();
+
+ public static Props Props(IHubContext hub) =>
+ Akka.Actor.Props.Create(() => new FleetStatusSignalRBridge(hub));
+
+ public FleetStatusSignalRBridge(IHubContext hub)
+ {
+ _hub = hub;
+ ReceiveAsync(ForwardAsync);
+ Receive(_ => { /* DPS confirmation */ });
+ }
+
+ protected override void PreStart() =>
+ DistributedPubSub.Get(Context.System).Mediator.Tell(new Subscribe(TopicName, Self));
+
+ private async Task ForwardAsync(FleetStatusChanged msg)
+ {
+ try
+ {
+ await _hub.Clients.All.SendAsync(FleetStatusHub.MethodName, msg);
+ }
+ catch (Exception ex)
+ {
+ _log.Warning(ex, "FleetStatusSignalRBridge: SignalR push failed (count={Count})", msg.Nodes.Count);
+ }
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3b1e118
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs
@@ -0,0 +1,39 @@
+using Akka.Actor;
+using Akka.Hosting;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
+
+public static class HubServiceCollectionExtensions
+{
+ public const string FleetStatusSignalRBridgeName = "fleet-status-signalr-bridge";
+
+ ///
+ /// Spawns the SignalR bridge actors that forward DPS messages to browser-facing SignalR
+ /// hubs. Currently: (DPS fleet-status topic →
+ /// clients).
+ ///
+ /// Call inside the admin-role configurator on the shared :
+ ///
+ /// if (hasAdmin)
+ /// {
+ /// ab.WithOtOpcUaControlPlaneSingletons();
+ /// ab.WithOtOpcUaSignalRBridges();
+ /// }
+ ///
+ ///
+ public static AkkaConfigurationBuilder WithOtOpcUaSignalRBridges(this AkkaConfigurationBuilder builder)
+ {
+ builder.WithActors((system, registry, resolver) =>
+ {
+ var hub = resolver.GetService>();
+ var actor = system.ActorOf(FleetStatusSignalRBridge.Props(hub), FleetStatusSignalRBridgeName);
+ registry.Register(actor);
+ });
+ return builder;
+ }
+}
+
+/// Marker key for lookup of the SignalR bridge actor.
+public sealed class FleetStatusSignalRBridgeKey { }
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
index a655b9f..b4faddb 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
@@ -44,7 +44,10 @@ builder.Services.AddAkka("otopcua", (ab, sp) =>
{
ab.WithOtOpcUaClusterBootstrap(sp);
if (hasAdmin)
+ {
ab.WithOtOpcUaControlPlaneSingletons();
+ ab.WithOtOpcUaSignalRBridges();
+ }
if (hasDriver)
ab.WithOtOpcUaRuntimeActors();
});