@if (!_connected)
@@ -263,6 +263,12 @@
private bool _loading = true;
private bool _connected;
private bool _connecting;
+
+ // True for the whole of LoadInstancesForSite. Blazor renders as soon as that load hits its
+ // first await, and the page is not _connected then — so without this the picker is enabled
+ // while it is still showing the PREVIOUS site's tree, and anything typed into it is wiped
+ // when the new lists land (the picker reads a new list object as "the site changed").
+ private bool _instancesLoading;
private bool _connectedFromStorage;
// Which tab pane is visible — "attributes" (default) or "alarms".
@@ -415,43 +421,74 @@
{
_selectedSiteId = siteId;
await LoadInstancesForSite();
+
+ // Membership guard, mirroring the ?instanceId= path above. A stored instance that
+ // has since been disabled (or deleted, or moved to another site) is not in the list
+ // the picker was handed, so it renders a blank field while the page holds a non-zero
+ // id and auto-connects to it anyway. Forget the stored instance instead — the site
+ // stays selected and loaded, so the operator just picks again.
+ var stored = _siteInstances.FirstOrDefault(i => i.Id == instanceId);
+ if (stored == null)
+ {
+ await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.instanceId");
+ _toast.ShowInfo(
+ "The instance from your previous session is no longer available for debug streaming.",
+ autoDismissMs: 8000);
+ StateHasChanged();
+ return;
+ }
+
_selectedInstanceId = instanceId;
_connectedFromStorage = true;
StateHasChanged();
await Connect();
// Auto-reconnect notice — the user didn't initiate this connection.
- var inst = _siteInstances.FirstOrDefault(i => i.Id == instanceId);
_toast.ShowInfo(
- $"Auto-reconnected to {inst?.UniqueName ?? "instance"} from previous session.",
+ $"Auto-reconnected to {stored.UniqueName} from previous session.",
autoDismissMs: 8000);
}
}
+ ///
+ /// Loads the selected site's instances and areas into the picker's two lists, holding
+ /// _instancesLoading for the whole round trip so the field cannot be typed into
+ /// while it is still showing the previous site's tree. The flag is cleared in a
+ /// finally: a scope refusal or a repository fault must not leave the picker
+ /// disabled forever.
+ ///
private async Task LoadInstancesForSite()
{
- _siteInstances = new();
- _siteAreas = new();
- _selectedInstanceId = 0;
- if (_selectedSiteId == 0) return;
- // Site scoping (CentralUI-002): re-check the claim server-side — a query
- // string or stale localStorage value could name a site outside the grant.
- if (!await SiteScope.IsSiteAllowedAsync(_selectedSiteId))
- {
- _selectedSiteId = 0;
- _toast.ShowError("You are not permitted to debug instances on that site.");
- return;
- }
+ _instancesLoading = true;
try
{
- _siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
- .Where(i => i.State == InstanceState.Enabled)
- .ToList();
- _siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
+ _siteInstances = new();
+ _siteAreas = new();
+ _selectedInstanceId = 0;
+ if (_selectedSiteId == 0) return;
+ // Site scoping (CentralUI-002): re-check the claim server-side — a query
+ // string or stale localStorage value could name a site outside the grant.
+ if (!await SiteScope.IsSiteAllowedAsync(_selectedSiteId))
+ {
+ _selectedSiteId = 0;
+ _toast.ShowError("You are not permitted to debug instances on that site.");
+ return;
+ }
+ try
+ {
+ _siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
+ .Where(i => i.State == InstanceState.Enabled)
+ .ToList();
+ _siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
+ }
+ catch (Exception ex)
+ {
+ _toast.ShowError($"Failed to load instances: {ex.Message}");
+ }
}
- catch (Exception ex)
+ finally
{
- _toast.ShowError($"Failed to load instances: {ex.Message}");
+ _instancesLoading = false;
}
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
index d5ab097f..1dfb1a66 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor
@@ -69,7 +69,7 @@
loaded once per site — the picker treats a new list as a site change. *@
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
index a2296c36..0c9608b9 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs
@@ -122,6 +122,16 @@ public partial class FlowDebugger : ComponentBase, IDisposable
/// The selected site's areas; the picker nests them into its tree. See .
private List _areas = [];
+
+ ///
+ /// True while is fetching a site's instances and areas, and
+ /// the sole reason the picker is disabled for that window. Blazor renders the moment the load
+ /// hits its first await, and by then is already the NEW site
+ /// while the two lists are still the OLD ones — so without this the field is enabled over
+ /// stale data, and anything typed into it is wiped when the lists land (the picker reads a new
+ /// list object as "the site changed" and clears its text).
+ ///
+ private bool _instancesLoading;
private List _scripts = [];
private int _selectedMethodId;
@@ -419,19 +429,33 @@ public partial class FlowDebugger : ComponentBase, IDisposable
await PredictAsync();
}
+ ///
+ /// Loads the selected site's instances and areas into the picker's two lists, holding
+ /// for the whole round trip so the field cannot be typed
+ /// into while it is still showing the previous site's tree. The flag is cleared in a
+ /// finally: a repository fault must not leave the picker disabled forever.
+ ///
private async Task LoadInstancesAsync()
{
- if (_selectedSiteId <= 0)
+ _instancesLoading = true;
+ try
{
- _instances = [];
- _areas = [];
- return;
- }
+ if (_selectedSiteId <= 0)
+ {
+ _instances = [];
+ _areas = [];
+ return;
+ }
- _instances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))
- .OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase).ToList();
- // The picker nests these itself (InstanceTreeBuilder), so order does not matter here.
- _areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_selectedSiteId)).ToList();
+ // Neither list is sorted here: the picker nests and orders both itself
+ // (InstanceTreeBuilder). The OrderBy this replaces dated from the flat