diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor index 4a2a7e105..2738af0bc 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor @@ -1,9 +1,11 @@ -@* Live driver-status panel — subscribes to /hubs/driverstatus and shows state chip, - last-success age, 5-min error count, and last error message. +@* Live driver-status panel — reads the in-process driver-health snapshot store and shows, PER REPORTING NODE + (lmxopcua#851 — both nodes of a redundant pair run the driver), the state chip, last-success age, 5-min error + count and last error message. A snapshot from a node that predates ReportingNodeId (#847) renders unlabelled. Enabled=false renders a static "Disabled" notice and never opens the hub. DriverOperator-gated Reconnect/Restart buttons appear for authorised users. *@ @implements IAsyncDisposable @using Microsoft.AspNetCore.Authorization +@using ZB.MOM.WW.OtOpcUa.AdminUI.Hosts @using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs @using ZB.MOM.WW.OtOpcUa.Commons.Interfaces @using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin @@ -14,12 +16,18 @@ @inject IDriverStatusSnapshotStore StatusStore @inject IDriverResilienceStatusStore ResilienceStore -
-
+@{ var nodes = _byNode?.Entries ?? []; var now = DateTime.UtcNow; } +
+
Driver status - @if (_snapshot is not null) + @if (nodes.Count > 0) { - @_snapshot.State + @foreach (var n in nodes) + { + @(nodes.Count > 1 || n.NodeId is not null ? $"{n.Label}: " : "")@n.Snapshot.State + } } else if (!Enabled) { @@ -40,37 +48,51 @@ {

SignalR error: @DisplayText.Abbreviate(_error, 60)

} - else if (_snapshot is null) + else if (nodes.Count == 0) {

Awaiting first snapshot…

} else { -
- - Last success: - @if (_snapshot.LastSuccessfulReadUtc is { } t) - { - @HumanizeAge(t) ago - } - else - { - never - } - - - @if (_snapshot.ErrorCount5Min > 0) - { - @_snapshot.ErrorCount5Min error@(_snapshot.ErrorCount5Min == 1 ? "" : "s") / 5 min - } -
- - @if (_snapshot.LastError is { Length: > 0 } lastError) + @foreach (var n in nodes) { -
- Last error -
@lastError
-
+ var snap = n.Snapshot; +
+
+ @n.Label + @snap.State + + Last success: + @if (snap.LastSuccessfulReadUtc is { } t) + { + @HumanizeAge(t) ago + } + else + { + never + } + + + @if (snap.ErrorCount5Min > 0) + { + @snap.ErrorCount5Min error@(snap.ErrorCount5Min == 1 ? "" : "s") / 5 min + } + @if (n.IsStale(now, StaleAfter)) + { + stale + } +
+ + @if (snap.LastError is { Length: > 0 } lastError) + { +
+ Last error +
@lastError
+
+ } +
} } @@ -194,9 +216,10 @@ [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public bool Enabled { get; set; } = true; - private DriverHealthChanged? _snapshot; - private DateTime _lastUpdateUtc = DateTime.MinValue; - private bool _stale; + // One entry per reporting node (lmxopcua#851). Created in OnInitializedAsync once DriverInstanceId is bound. + // Staleness is per node: a node that stops publishing dims on its own while its peer stays live. + private DriverStatusByNode? _byNode; + private static readonly TimeSpan StaleAfter = TimeSpan.FromSeconds(30); // Per-host resilience snapshots for THIS instance, keyed by HostName. Fed by the // driver-resilience-status DPS store (read in-process, same house pattern as the health store). @@ -234,13 +257,11 @@ _connecting = true; - // Tick every 5 s to refresh the stale-dimming check and humanized ages. - _timer = new System.Threading.Timer(_ => - { - _stale = _snapshot is not null && - (DateTime.UtcNow - _lastUpdateUtc).TotalSeconds > 30; - InvokeAsync(StateHasChanged); - }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + _byNode = new DriverStatusByNode(DriverInstanceId); + + // Tick every 5 s to re-render the per-node stale dimming and humanized ages. + _timer = new System.Threading.Timer(_ => InvokeAsync(StateHasChanged), + null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); // Read live status straight from the in-process snapshot store rather than opening a // self-targeted SignalR connection. This component runs server-side (Blazor @@ -252,12 +273,12 @@ // this circuit. try { + // Subscribe BEFORE priming so a publish racing the read is not missed; a primed copy that lands after + // a newer racing live one is dropped by Upsert (it keeps the later PublishedUtc per node). StatusStore.SnapshotChanged += OnSnapshotChanged; - if (StatusStore.TryGet(DriverInstanceId, out var snap)) - { - _snapshot = snap; - _lastUpdateUtc = DateTime.UtcNow; - } + var primedUtc = DateTime.UtcNow; + foreach (var snap in StatusStore.GetAll()) + _byNode.Upsert(snap, primedUtc); // Prime + subscribe the resilience store (same in-process read pattern as the health store). ResilienceStore.SnapshotChanged += OnResilienceChanged; @@ -278,12 +299,9 @@ // ignore snapshots for other instances and marshal onto the render sync context. private void OnSnapshotChanged(DriverHealthChanged snap) { - if (!string.Equals(snap.DriverInstanceId, DriverInstanceId, StringComparison.Ordinal)) + if (_byNode is null || !_byNode.Upsert(snap, DateTime.UtcNow)) return; - _snapshot = snap; - _lastUpdateUtc = DateTime.UtcNow; - _stale = false; InvokeAsync(StateHasChanged); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/DriverStatusByNode.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/DriverStatusByNode.cs new file mode 100644 index 000000000..e13d30a17 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/DriverStatusByNode.cs @@ -0,0 +1,86 @@ +using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hosts; + +/// +/// One reporting node's last-known health for a driver, as the Controls dialog's +/// DriverStatusPanel renders it (lmxopcua#851). +/// +/// The reporting ClusterNode.NodeId; null when the snapshot came from a node that +/// predates (#847) — it still renders, unlabelled. +/// The node's most recent snapshot. +/// When THIS AdminUI node received it (local clock — drives the stale dimming, so a +/// peer's clock skew cannot make a live row look stale). +public sealed record DriverNodeStatus(string? NodeId, DriverHealthChanged Snapshot, DateTime ReceivedUtc) +{ + /// Render key, unique per reporting node (the empty string stands for "no node id"). + public string Key => NodeId ?? ""; + + /// The label the panel prints for the node. + public string Label => NodeId ?? "—"; + + /// True once no snapshot has arrived from this node for longer than . + public bool IsStale(DateTime nowUtc, TimeSpan staleAfter) => nowUtc - ReceivedUtc > staleAfter; +} + +/// +/// The per-node health view of ONE driver instance behind the Controls dialog's status panel (lmxopcua#851). +/// Both nodes of a redundant pair run the same driver instance, so the panel shows one entry per reporting +/// node instead of the single most-recently-published snapshot (which flipped between a Faulted node and a +/// Healthy peer on every publish). Thread-safe: is called on the snapshot store's bridge +/// thread while the render reads . +/// +public sealed class DriverStatusByNode +{ + private readonly object _gate = new(); + private readonly Dictionary _byNode = new(StringComparer.Ordinal); + + /// Initializes a new view for one driver instance. + /// The driver instance whose snapshots this view keeps; others are ignored. + public DriverStatusByNode(string driverInstanceId) => DriverInstanceId = driverInstanceId; + + /// The driver instance this view tracks. + public string DriverInstanceId { get; } + + /// + /// Records a snapshot as its node's latest. Snapshots for other driver instances are ignored, and so is one + /// published EARLIER than the node's current entry (both stamped by the same node's clock): the panel primes + /// from GetAll() after subscribing, so a live publish can land before the primed copy of an older + /// one. Returns true when the view changed. + /// + /// The published snapshot. + /// Local receive time. + public bool Upsert(DriverHealthChanged snapshot, DateTime receivedUtc) + { + if (!string.Equals(snapshot.DriverInstanceId, DriverInstanceId, StringComparison.Ordinal)) + return false; + + var nodeId = string.IsNullOrWhiteSpace(snapshot.ReportingNodeId) ? null : snapshot.ReportingNodeId; + var entry = new DriverNodeStatus(nodeId, snapshot, receivedUtc); + lock (_gate) + { + if (_byNode.TryGetValue(entry.Key, out var current) && snapshot.PublishedUtc < current.Snapshot.PublishedUtc) + return false; + _byNode[entry.Key] = entry; + } + + return true; + } + + /// + /// A point-in-time copy of every node's entry, ordered by node id (ordinal, case-insensitive) with the + /// unlabelled (pre-#847) entry last. + /// + public IReadOnlyList Entries + { + get + { + DriverNodeStatus[] copy; + lock (_gate) copy = _byNode.Values.ToArray(); + return copy + .OrderBy(e => e.NodeId is null ? 1 : 0) + .ThenBy(e => e.NodeId ?? "", StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Hosts/DriverStatusByNodeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Hosts/DriverStatusByNodeTests.cs new file mode 100644 index 000000000..a26121302 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Hosts/DriverStatusByNodeTests.cs @@ -0,0 +1,140 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Hosts; +using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; +using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Hosts; + +/// +/// Covers — the per-node view behind the /hosts Controls dialog's +/// DriverStatusPanel (lmxopcua#851). Before it the panel showed ONE snapshot per driver, the most +/// recently published across both nodes of the redundant pair, so a driver Faulted on central-1 and Healthy on +/// central-2 flipped between the two on every publish. +/// +[Trait("Category", "Unit")] +public sealed class DriverStatusByNodeTests +{ + private static readonly DateTime T0 = new(2026, 11, 11, 12, 0, 0, DateTimeKind.Utc); + + private static DriverHealthChanged Snap(string driver, string state, DateTime published, string? node, string? error = null) => + new("MAIN", driver, state, null, error, error is null ? 0 : 1, published, ReportingNodeId: node); + + [Fact] + public void Two_nodes_reporting_one_driver_yield_two_entries_ordered_by_node() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Healthy", T0, "central-2"), T0).ShouldBeTrue(); + view.Upsert(Snap("drv", "Faulted", T0.AddSeconds(1), "central-1", "boom"), T0).ShouldBeTrue(); + + var entries = view.Entries; + entries.Select(e => e.NodeId).ShouldBe(["central-1", "central-2"]); + entries.Select(e => e.Snapshot.State).ShouldBe(["Faulted", "Healthy"]); + entries.Select(e => e.Key).Distinct().Count().ShouldBe(2); + } + + [Fact] + public void A_later_publish_replaces_only_its_own_nodes_entry() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Faulted", T0, "central-1", "x"), T0); + view.Upsert(Snap("drv", "Healthy", T0, "central-2"), T0); + view.Upsert(Snap("drv", "Faulted", T0.AddSeconds(5), "central-1", "y"), T0.AddSeconds(5)); + + var entries = view.Entries; + entries.Count.ShouldBe(2); + entries.Single(e => e.NodeId == "central-1").Snapshot.LastError.ShouldBe("y"); + entries.Single(e => e.NodeId == "central-2").Snapshot.State.ShouldBe("Healthy"); + } + + [Fact] + public void Snapshots_for_other_drivers_are_ignored() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("other", "Faulted", T0, "central-1"), T0).ShouldBeFalse(); + view.Entries.ShouldBeEmpty(); + } + + [Fact] + public void A_snapshot_from_a_pre_847_node_with_no_node_id_still_renders_unlabelled_and_sorts_last() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Healthy", T0, node: null), T0).ShouldBeTrue(); + view.Upsert(Snap("drv", "Faulted", T0, "central-1"), T0); + + var entries = view.Entries; + entries.Count.ShouldBe(2); + entries[0].NodeId.ShouldBe("central-1"); + entries[1].NodeId.ShouldBeNull(); + entries[1].Label.ShouldBe("—"); + entries[1].Key.ShouldBe(""); + entries[1].Snapshot.State.ShouldBe("Healthy"); + } + + [Fact] + public void A_blank_node_id_is_the_same_unlabelled_entry_as_a_missing_one() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Healthy", T0, node: null), T0); + view.Upsert(Snap("drv", "Faulted", T0.AddSeconds(1), node: " "), T0); + + view.Entries.ShouldHaveSingleItem().Snapshot.State.ShouldBe("Faulted"); + } + + [Fact] + public void An_older_publish_arriving_late_does_not_overwrite_a_newer_one_for_the_same_node() + { + // The panel subscribes, THEN primes from the store's GetAll(): a live publish can land before the primed + // copy of the previous snapshot. The primed (older) copy must not win. + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Healthy", T0.AddSeconds(10), "central-1"), T0).ShouldBeTrue(); + view.Upsert(Snap("drv", "Faulted", T0, "central-1"), T0).ShouldBeFalse(); + + view.Entries.ShouldHaveSingleItem().Snapshot.State.ShouldBe("Healthy"); + } + + [Fact] + public void Staleness_is_per_node() + { + var view = new DriverStatusByNode("drv"); + view.Upsert(Snap("drv", "Healthy", T0, "central-1"), receivedUtc: T0); + view.Upsert(Snap("drv", "Healthy", T0, "central-2"), receivedUtc: T0.AddSeconds(40)); + + var now = T0.AddSeconds(45); + var stale = TimeSpan.FromSeconds(30); + var entries = view.Entries; + entries.Single(e => e.NodeId == "central-1").IsStale(now, stale).ShouldBeTrue(); + entries.Single(e => e.NodeId == "central-2").IsStale(now, stale).ShouldBeFalse(); + } + + [Fact] + public void Priming_from_the_store_picks_up_every_node_of_the_driver() + { + // The store keys per (driver, node) since #847; the panel primes from GetAll(), not TryGet() (which + // answers only the most recent across nodes). + var store = new InMemoryDriverStatusSnapshotStore(); + store.Upsert(Snap("drv", "Faulted", T0, "central-1", "boom")); + store.Upsert(Snap("drv", "Healthy", T0.AddSeconds(1), "central-2")); + store.Upsert(Snap("other", "Healthy", T0, "central-1")); + + var view = new DriverStatusByNode("drv"); + foreach (var s in store.GetAll()) view.Upsert(s, T0); + + view.Entries.Select(e => $"{e.NodeId}:{e.Snapshot.State}").ShouldBe(["central-1:Faulted", "central-2:Healthy"]); + } + + [Fact] + public void The_status_panel_renders_per_node_entries_and_primes_from_GetAll() + { + var src = AdminUiSource.Read("Components", "Shared", "Drivers", "DriverStatusPanel.razor"); + + src.ShouldContain("new DriverStatusByNode(DriverInstanceId)"); + src.ShouldContain("StatusStore.GetAll()"); + src.ShouldNotContain("StatusStore.TryGet("); + src.ShouldContain("@foreach (var n in nodes)"); + src.ShouldContain("@n.Label"); + src.ShouldContain("@key=\"@($\"node-{n.Key}\")\""); + // The old single-snapshot field must be gone, or a half-migrated panel could still render one node. + src.ShouldNotContain("_snapshot"); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs new file mode 100644 index 000000000..f0d096ce8 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs @@ -0,0 +1,126 @@ +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1; +using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry; +using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry.Pre847; + +namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry; + +/// +/// Mixed-version wire tests for DriverHealth.reporting_node_id (proto field 15, added by lmxopcua#847; +/// tests lmxopcua#851). A rolling upgrade runs old and new nodes against old and new centrals, so the field +/// must cross the wire in all three directions: new→new round-trips, old→new parses as ABSENT (not an empty +/// node id), and new→old is ignored as an unknown field without disturbing the rest of the message. "Old" is +/// the frozen pre-#847 schema in Protos/telemetry_pre847.proto — a real generated parser/serializer, not +/// a hand-built byte string. +/// +[Trait("Category", "Unit")] +public sealed class DriverHealthReportingNodeWireTests +{ + private static readonly DateTime PublishedUtc = new(2026, 11, 11, 12, 0, 0, DateTimeKind.Utc); + private static readonly DateTime LastReadUtc = new(2026, 11, 11, 11, 59, 30, DateTimeKind.Utc); + + private static DriverHealth NewHealth(string? reportingNodeId) + { + var msg = new DriverHealth + { + ClusterId = "MAIN", + DriverInstanceId = "drv-1", + State = "Faulted", + LastSuccessfulReadUtc = Timestamp.FromDateTime(LastReadUtc), + LastError = "timeout", + ErrorCount5Min = 4, + PublishedUtc = Timestamp.FromDateTime(PublishedUtc), + HasDiagnostics = true, + }; + msg.Diagnostics["pdu"] = "480"; + if (reportingNodeId is not null) msg.ReportingNodeId = reportingNodeId; + return msg; + } + + [Fact] + public void New_sender_to_new_receiver_round_trips_the_reporting_node_id() + { + var bytes = NewHealth("central-1").ToByteArray(); + + var parsed = DriverHealth.Parser.ParseFrom(bytes); + parsed.HasReportingNodeId.ShouldBeTrue(); + + var e = TelemetryProtoMapCentral.ToHealth(parsed); + e.ReportingNodeId.ShouldBe("central-1"); + e.DriverInstanceId.ShouldBe("drv-1"); + e.State.ShouldBe("Faulted"); + e.PublishedUtc.ShouldBe(PublishedUtc); + } + + [Fact] + public void New_sender_without_a_node_id_omits_field_15_and_maps_to_null() + { + // A harness / node with no role info leaves the field unset: it must not appear on the wire at all, so an + // explicit "" would not be confused with "absent" either way. + var msg = NewHealth(reportingNodeId: null); + msg.HasReportingNodeId.ShouldBeFalse(); + + var e = TelemetryProtoMapCentral.ToHealth(DriverHealth.Parser.ParseFrom(msg.ToByteArray())); + e.ReportingNodeId.ShouldBeNull(); + } + + [Fact] + public void Old_sender_to_new_receiver_parses_as_absent_node_id_and_keeps_every_other_field() + { + var old = new DriverHealthPre847 + { + ClusterId = "MAIN", + DriverInstanceId = "drv-1", + State = "Healthy", + LastSuccessfulReadUtc = Timestamp.FromDateTime(LastReadUtc), + ErrorCount5Min = 0, + PublishedUtc = Timestamp.FromDateTime(PublishedUtc), + RediscoveryReason = "schema changed", + HasDiagnostics = true, + }; + old.Diagnostics["pdu"] = "240"; + + var parsed = DriverHealth.Parser.ParseFrom(old.ToByteArray()); + + // Explicit presence: absent, NOT the proto3 default "" — central keys the snapshot under the + // driver-only (unlabelled) node and the AdminUI renders it as a pre-#847 row. + parsed.HasReportingNodeId.ShouldBeFalse(); + var e = TelemetryProtoMapCentral.ToHealth(parsed); + e.ReportingNodeId.ShouldBeNull(); + e.ClusterId.ShouldBe("MAIN"); + e.DriverInstanceId.ShouldBe("drv-1"); + e.State.ShouldBe("Healthy"); + e.LastSuccessfulReadUtc.ShouldBe(LastReadUtc); + e.LastError.ShouldBeNull(); + e.PublishedUtc.ShouldBe(PublishedUtc); + e.RediscoveryReason.ShouldBe("schema changed"); + e.Diagnostics.ShouldNotBeNull(); + e.Diagnostics!["pdu"].ShouldBe("240"); + } + + [Fact] + public void New_sender_to_old_receiver_ignores_field_15_and_keeps_every_other_field() + { + var bytes = NewHealth("central-2").ToByteArray(); + + // An old central's parser: field 15 is unknown to it. It must neither throw nor shift any known field. + var old = DriverHealthPre847.Parser.ParseFrom(bytes); + + old.ClusterId.ShouldBe("MAIN"); + old.DriverInstanceId.ShouldBe("drv-1"); + old.State.ShouldBe("Faulted"); + old.LastSuccessfulReadUtc.ToDateTime().ShouldBe(LastReadUtc); + old.LastError.ShouldBe("timeout"); + old.ErrorCount5Min.ShouldBe(4); + old.PublishedUtc.ToDateTime().ShouldBe(PublishedUtc); + old.HasDiagnostics.ShouldBeTrue(); + old.Diagnostics["pdu"].ShouldBe("480"); + + // The old parser keeps field 15 as an unknown field rather than corrupting it: re-serialised by an old + // process, a new reader still recovers the node id. + DriverHealth.Parser.ParseFrom(old.ToByteArray()).ReportingNodeId.ShouldBe("central-2"); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto new file mode 100644 index 000000000..87bda4139 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +// TEST-ONLY frozen copy of telemetry.v1.DriverHealth as it stood BEFORE lmxopcua#847 added +// `optional string reporting_node_id = 15`. It stands in for an old node / an old central in the mixed-version +// wire tests (lmxopcua#851): the same field numbers and wire types as the real message, minus field 15. Never edit +// it to track telemetry.proto — its whole point is to stay old. +package telemetry.tests.pre847; + +option csharp_namespace = "ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry.Pre847"; + +import "google/protobuf/timestamp.proto"; + +message DriverHealthPre847 { + string cluster_id = 1; + string driver_instance_id = 2; + string state = 3; + google.protobuf.Timestamp last_successful_read_utc = 4; + optional string last_error = 5; + int32 error_count_5min = 6; + google.protobuf.Timestamp published_utc = 7; + google.protobuf.Timestamp rediscovery_needed_utc = 8; + optional string rediscovery_reason = 9; + bool has_host_statuses = 10; + repeated HostConnectivityPre847 host_statuses = 11; + SkippedTagsPre847 skipped_tags = 12; + bool has_diagnostics = 13; + map diagnostics = 14; +} + +message SkippedTagsPre847 { + int32 authored_count = 1; + int32 skipped_count = 2; + repeated SkippedTagPre847 sample = 3; +} + +message SkippedTagPre847 { + string raw_path = 1; + string reason = 2; +} + +message HostConnectivityPre847 { + string host_name = 1; + string state = 2; + google.protobuf.Timestamp last_changed_utc = 3; +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj index 9b1252e6b..9608a8300 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj @@ -11,6 +11,12 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -18,6 +24,10 @@ + + + +