diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor
index 8280a50a..7a98209b 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor
@@ -445,6 +445,18 @@
});
});
+ // M2.11: the site returns InstanceNotFound=true when the instance is
+ // not deployed there (e.g. deployment not yet pushed, or wrong site).
+ if (session.InitialSnapshot.InstanceNotFound)
+ {
+ DebugStreamService.StopStream(session.SessionId);
+ _toast.ShowError(
+ $"Instance is not deployed on that site. " +
+ $"Deploy it first or choose the correct site.");
+ _connecting = false;
+ return;
+ }
+
_session = session;
// Populate initial state from snapshot
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs
index f92a90cd..ba1f8695 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs
@@ -2,8 +2,38 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
+///
+/// Snapshot of an instance's debug state returned in response to a
+/// or .
+///
+///
+///
+/// Additive-only contract (M2.11): is an
+/// optional trailing parameter with a default of so every
+/// existing positional constructor call and every existing serialized wire frame
+/// remains valid. Callers that receive a snapshot with
+/// InstanceNotFound = true know the instance was unknown on the site and
+/// should distinguish that from a deployed-but-empty instance
+/// (InstanceNotFound = false, empty and
+/// ).
+///
+///
+/// A new dedicated message type (DebugViewInstanceNotFound) was
+/// considered but rejected: the ClusterClient / ClusterClientReceptionist
+/// channel is typed on the request side and the bridge actor is already
+/// pattern-matching on DebugViewSnapshot for the initial-snapshot TCS
+/// in DebugStreamService. Introducing a second reply type would require
+/// every consumer to handle an additional Ask result union — more change
+/// for no additive-safety gain. The defaulted field is strictly additive and
+/// keeps all call sites untouched.
+///
+///
public record DebugViewSnapshot(
string InstanceUniqueName,
IReadOnlyList AttributeValues,
IReadOnlyList AlarmStates,
- DateTimeOffset SnapshotTimestamp);
+ DateTimeOffset SnapshotTimestamp,
+ // M2.11 — additive field: true when the requested instance is not registered
+ // on this site. Defaults to false so all existing call sites and wire
+ // frames are unaffected.
+ bool InstanceNotFound = false);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs
index c4474739..7ac30fc1 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs
@@ -85,9 +85,23 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
_grpcNodeAAddress = grpcNodeAAddress;
_grpcNodeBAddress = grpcNodeBAddress;
- // Initial snapshot response from the site (via ClusterClient)
+ // Initial snapshot response from the site (via ClusterClient).
+ // M2.11: if the site reports InstanceNotFound=true the instance is not
+ // deployed there. Forward the snapshot (with InstanceNotFound=true) to
+ // _onEvent so DebugStreamService's TCS resolves and the caller can
+ // inspect the flag; then stop cleanly without opening a gRPC stream.
Receive(snapshot =>
{
+ if (snapshot.InstanceNotFound)
+ {
+ _log.Warning("Instance {0} is not deployed on site; terminating debug stream",
+ _instanceUniqueName);
+ _stopped = true;
+ _onEvent(snapshot); // resolves the snapshot TCS with InstanceNotFound=true
+ Context.Stop(Self);
+ return;
+ }
+
_log.Info("Received initial snapshot for {0} ({1} attrs, {2} alarms)",
_instanceUniqueName, snapshot.AttributeValues.Count, snapshot.AlarmStates.Count);
_onEvent(snapshot);
diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
index 10a6aae6..d412572e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
@@ -895,11 +895,14 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
else
{
+ // M2.11: set InstanceNotFound=true so the caller can distinguish
+ // "not deployed on this site" from a deployed-but-empty instance.
_logger.LogWarning(
"Debug view subscribe for unknown instance {Instance}", request.InstanceUniqueName);
Sender.Tell(new DebugViewSnapshot(
request.InstanceUniqueName, Array.Empty(),
- Array.Empty(), DateTimeOffset.UtcNow));
+ Array.Empty(), DateTimeOffset.UtcNow,
+ InstanceNotFound: true));
}
}
@@ -919,11 +922,14 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
else
{
+ // M2.11: set InstanceNotFound=true so the caller can distinguish
+ // "not deployed on this site" from a deployed-but-empty instance.
_logger.LogWarning(
"Debug snapshot for unknown instance {Instance}", request.InstanceUniqueName);
Sender.Tell(new DebugViewSnapshot(
request.InstanceUniqueName, Array.Empty(),
- Array.Empty(), DateTimeOffset.UtcNow));
+ Array.Empty(), DateTimeOffset.UtcNow,
+ InstanceNotFound: true));
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerActorTests.cs
index 6b57e936..062592af 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerActorTests.cs
@@ -3,6 +3,7 @@ using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
@@ -387,4 +388,64 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
Assert.Equal("route-corr-2", response.CorrelationId);
Assert.True(response.Success, $"Routed call failed: {response.ErrorMessage}");
}
+
+ // ── M2.11: Debug-view routing — unknown-instance not-found signal ──
+
+ [Fact]
+ public async Task RouteDebugSnapshot_UnknownInstance_SetsInstanceNotFound()
+ {
+ // An instance that was never deployed to this site must return a
+ // DebugViewSnapshot with InstanceNotFound=true so the caller can
+ // distinguish "not deployed here" from a deployed-but-empty instance.
+ var actor = CreateDeploymentManager();
+ await Task.Delay(500);
+
+ actor.Tell(new DebugSnapshotRequest("NeverDeployed", "snap-corr-1"));
+
+ var snapshot = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("NeverDeployed", snapshot.InstanceUniqueName);
+ Assert.True(snapshot.InstanceNotFound,
+ "Expected InstanceNotFound=true for an instance not registered on this site.");
+ Assert.Empty(snapshot.AttributeValues);
+ Assert.Empty(snapshot.AlarmStates);
+ }
+
+ [Fact]
+ public async Task RouteDebugViewSubscribe_UnknownInstance_SetsInstanceNotFound()
+ {
+ // Same contract for the subscribe path: unknown instance → InstanceNotFound=true.
+ var actor = CreateDeploymentManager();
+ await Task.Delay(500);
+
+ actor.Tell(new SubscribeDebugViewRequest("NeverDeployed2", "sub-corr-1"));
+
+ var snapshot = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("NeverDeployed2", snapshot.InstanceUniqueName);
+ Assert.True(snapshot.InstanceNotFound,
+ "Expected InstanceNotFound=true for an instance not registered on this site.");
+ Assert.Empty(snapshot.AttributeValues);
+ Assert.Empty(snapshot.AlarmStates);
+ }
+
+ [Fact]
+ public async Task RouteDebugSnapshot_KnownButEmptyInstance_DoesNotSetInstanceNotFound()
+ {
+ // A deployed instance with no runtime data yet must return an empty
+ // snapshot with InstanceNotFound=false — the known-empty path is unchanged.
+ var actor = CreateDeploymentManager();
+ await Task.Delay(500);
+
+ actor.Tell(new DeployInstanceCommand(
+ "dep-dbg", "EmptyPump", "sha256:dbg",
+ MakeConfigJson("EmptyPump"), "admin", DateTimeOffset.UtcNow));
+ ExpectMsg(TimeSpan.FromSeconds(5));
+ await Task.Delay(800); // let InstanceActor spin up
+
+ actor.Tell(new DebugSnapshotRequest("EmptyPump", "snap-corr-2"));
+
+ var snapshot = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("EmptyPump", snapshot.InstanceUniqueName);
+ Assert.False(snapshot.InstanceNotFound,
+ "A deployed (but empty) instance must NOT set InstanceNotFound.");
+ }
}