diff --git a/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json b/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json index 1053d3e8..a63899c7 100644 --- a/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json +++ b/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json @@ -9,7 +9,7 @@ { "id": "T5", "subject": "Host: DriverResilienceStatusPublisherService (BuildMessages mapping + 5s DPS timer shell; TDD mapping in Host.IntegrationTests)", "status": "completed", "blockedBy": ["T1", "T4"] }, { "id": "T6", "subject": "Host DI: register the publisher in AddOtOpcUaDriverFactories + ResilienceStatusReaderRegistrationTests wiring guard (tracker HAS a production reader)", "status": "completed", "blockedBy": ["T5"] }, { "id": "T7", "subject": "AdminUI: IDriverResilienceStatusStore + InMemory impl + AddOtOpcUaDriverStatusServices registration (TDD mirroring DriverStatusSnapshotStoreTests)", "status": "completed", "blockedBy": ["T4"] }, - { "id": "T8", "subject": "AdminUI: DriverResilienceStatusBridge DPS actor + spawn in WithOtOpcUaSignalRBridges", "status": "pending", "blockedBy": ["T7"] }, + { "id": "T8", "subject": "AdminUI: DriverResilienceStatusBridge DPS actor + spawn in WithOtOpcUaSignalRBridges", "status": "completed", "blockedBy": ["T7"] }, { "id": "T9", "subject": "AdminUI: resilience chip section in DriverStatusPanel.razor (in-process store read; no bUnit — verified by T10)", "status": "pending", "blockedBy": ["T7"] }, { "id": "T10", "subject": "Live-/run gate on docker-dev: rebuild BOTH centrals, S7 dead-endpoint breaker-open, both-replica chips, recovery clear, staleness dim", "status": "pending", "blockedBy": ["T6", "T8", "T9"] }, { "id": "T11", "subject": "Docs + review ledger: mark 03/U10 + 04/U-5 remediated; update STATUS.md + FOLLOWUP-10", "status": "pending", "blockedBy": ["T10"] } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverResilienceStatusBridge.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverResilienceStatusBridge.cs new file mode 100644 index 00000000..8b488327 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverResilienceStatusBridge.cs @@ -0,0 +1,39 @@ +using Akka.Actor; +using Akka.Cluster.Tools.PublishSubscribe; +using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; + +/// +/// Akka actor that subscribes to the driver-resilience-status DistributedPubSub topic and +/// upserts every snapshot into the in-process +/// . The driver status panel's resilience section reads +/// that store directly (per the house ban on server-side self-targeted SignalR connections), so — +/// unlike — there is no SignalR hub here; resilience has no +/// browser-JS consumer. Spawned per admin-role node by WithOtOpcUaSignalRBridges. +/// +public sealed class DriverResilienceStatusBridge : ReceiveActor +{ + public const string TopicName = DriverResilienceStatusChanged.TopicName; + + private readonly IDriverResilienceStatusStore _store; + + /// Creates actor props for a . + /// Resilience status store updated on every forwarded snapshot. + /// An Akka.NET instance for spawning the bridge actor. + public static Props Props(IDriverResilienceStatusStore store) => + Akka.Actor.Props.Create(() => new DriverResilienceStatusBridge(store)); + + /// Initializes a new instance of . + /// Resilience status store updated on every forwarded snapshot. + public DriverResilienceStatusBridge(IDriverResilienceStatusStore store) + { + _store = store; + Receive(msg => _store.Upsert(msg)); + Receive(_ => { /* DPS confirmation */ }); + } + + /// + protected override void PreStart() => + DistributedPubSub.Get(Context.System).Mediator.Tell(new Subscribe(TopicName, Self)); +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs index b77d7048..f3675564 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static class HubServiceCollectionExtensions public const string AlertSignalRBridgeName = "alert-signalr-bridge"; public const string ScriptLogSignalRBridgeName = "script-log-signalr-bridge"; public const string DriverStatusSignalRBridgeName = "driver-status-signalr-bridge"; + public const string DriverResilienceStatusBridgeName = "driver-resilience-status-bridge"; /// /// Registers the in-process live-push services the AdminUI's Blazor Server panels read @@ -71,6 +72,12 @@ public static class HubServiceCollectionExtensions var driverStatusStore = resolver.GetService(); var driverStatusBridge = system.ActorOf(DriverStatusSignalRBridge.Props(driverStatusHub, driverStatusStore), DriverStatusSignalRBridgeName); registry.Register(driverStatusBridge); + + // Resilience-status bridge: DPS topic -> in-process store (no SignalR hub — the panel reads + // the store directly, and resilience has no browser-JS consumer). + var resilienceStore = resolver.GetService(); + var resilienceBridge = system.ActorOf(DriverResilienceStatusBridge.Props(resilienceStore), DriverResilienceStatusBridgeName); + registry.Register(resilienceBridge); }); return builder; } @@ -81,3 +88,4 @@ public sealed class FleetStatusSignalRBridgeKey { } public sealed class AlertSignalRBridgeKey { } public sealed class ScriptLogSignalRBridgeKey { } public sealed class DriverStatusSignalRBridgeKey { } +public sealed class DriverResilienceStatusBridgeKey { }