diff --git a/docs/requirements/Component-CentralUI.md b/docs/requirements/Component-CentralUI.md
index 9eacaf94..c65d927b 100644
--- a/docs/requirements/Component-CentralUI.md
+++ b/docs/requirements/Component-CentralUI.md
@@ -93,7 +93,9 @@ Central cluster only. Sites have no user interface.
### Instance Management (Deployment Role)
- Create instances from templates at a specific site.
- Assign instances to areas.
-- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **Bulk assignment** supported: select multiple attributes and assign a data connection to all of them at once.
+- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **Bulk assignment** supported: select multiple attributes and assign a data connection to all of them at once. Each row also exposes:
+ - **Override** — optional per-attribute OPC UA node id (or other protocol address). When set, replaces the template's `DataSourceReference` at flattening time; when blank, the template default is used. The greyed placeholder shows the template default for context.
+ - **Browse…** — opens the OPC UA Tag Browser dialog, populated live from the site's OPC UA server via `BrowseOpcUaNodeCommand`. Visible only when the row's connection uses the OPC UA protocol; disabled until a connection is picked on that row. The dialog lazy-loads the address space, supports manual node-id entry as a fallback, and remains usable when the site or its OPC UA session is offline (the manual-paste field stays active even on error).
- Set instance-level attribute overrides (non-locked attributes only).
- Filter/search instances by site, area, template, or status.
- **Disable** instances — stops data collection, script triggers, and alarm evaluation at the site while retaining the deployed configuration.
diff --git a/docs/requirements/Component-DataConnectionLayer.md b/docs/requirements/Component-DataConnectionLayer.md
index 2e909e96..26c72903 100644
--- a/docs/requirements/Component-DataConnectionLayer.md
+++ b/docs/requirements/Component-DataConnectionLayer.md
@@ -140,6 +140,14 @@ These are configured via `DataConnectionOptions` in `appsettings.json`, not per-
- The existing subscription picks up the confirmed new value from the device and delivers it back to the Instance Actor as a standard value update.
- The Instance Actor's in-memory value is **not** updated until the device confirms the write.
+## Browsing the address space
+
+DCL is a clean data pipe on the hot path. Browse is an **opt-in capability** for protocols that support it, exposed via `IBrowsableDataConnection`. Only consumed by management/UI (the OPC UA tag picker on the instance configure page); Instance Actors never call it.
+
+- `OpcUaDataConnection` implements `IBrowsableDataConnection`; custom protocols do not.
+- `DataConnectionManagerActor` handles `BrowseOpcUaNodeCommand` (fields: `DataConnectionId`, `ParentNodeId`) and replies with `BrowseOpcUaNodeResult` (children + `Truncated` + structured `BrowseFailure?`).
+- Browse runs against the live session; no caching at DCL.
+
## Value Update Message Format
Each value update delivered to an Instance Actor includes:
diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md
index f212ef47..09d0c891 100644
--- a/docs/requirements/Component-TemplateEngine.md
+++ b/docs/requirements/Component-TemplateEngine.md
@@ -97,7 +97,7 @@ breadcrumb.
Override and lock rules apply per entity type at the following granularity:
-- **Attributes**: Value and Description are overridable. Data Type and Data Source Reference are fixed by the defining level. Lock applies to the entire attribute (when locked, no fields can be overridden).
+- **Attributes**: Value and Description are overridable. Data Type is fixed by the defining level. `DataSourceReference` on a template attribute defines the **default** physical address for that attribute. Instances may override per attribute via `InstanceConnectionBinding.DataSourceReferenceOverride`; the override replaces the template default at flattening time. When the override is null (the default), the template value is used. Lock applies to the entire attribute (when locked, no fields can be overridden).
- **Alarms**: Priority Level, Trigger Definition (thresholds/ranges/rates), Description, and On-Trigger Script reference are overridable. Name and Trigger Type (Value Match vs. Range vs. Rate of Change) are fixed. Lock applies to the entire alarm.
- **Scripts**: C# source code, Trigger configuration, minimum time between runs, and parameter/return definitions are overridable. Name is fixed. Lock applies to the entire script.
- **Composed module members**: A composing template or child template can override non-locked members inside a composed module using the canonical path-qualified name.
@@ -166,6 +166,8 @@ Each flattened configuration output includes a **revision hash** (computed from
- Staleness detection: comparing the deployed revision to the current template-derived revision without a full diff.
- Diff correlation: ensuring diffs are computed against a consistent baseline.
+The override flows into the flattened attribute's `DataSourceReference` and therefore participates in the revision hash — changes to an instance's binding overrides re-deploy as expected.
+
### On-Demand Validation
The same validation logic is available to Design users in the Central UI without triggering a deployment. This allows template authors to check their work for errors during authoring.
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor
new file mode 100644
index 00000000..d1556d41
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/OpcUaBrowserDialog.razor
@@ -0,0 +1,208 @@
+@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
+@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
+@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
+@inject IOpcUaBrowseService BrowseService
+
+@if (_isVisible)
+{
+
+ @* Test Bindings: one-shot live read of every bound attribute
+ whose row has a connection picked AND an effective tag
+ path. Disabled when no testable rows. Currently OPC UA
+ only — other protocols (none yet) would need their own
+ wire+adapter support to round-trip through ReadTagValuesCommand. *@
+
}
@@ -331,6 +364,19 @@
+
+ @* OPC UA Tag Browser dialog (Task 18) — rendered once; OpenBrowser
+ tracks which binding row's override input receives the picked node id. *@
+
+
+ @* Test Bindings dialog — one-shot live read of every bound attribute.
+ Method-arg ShowAsync(siteId, rows) — no Razor parameter propagation
+ race (same pattern as OpcUaBrowserDialog). *@
+
}
@@ -350,8 +396,28 @@
private List _bindingDataSourceAttrs = new();
private List _siteConnections = new();
private Dictionary _bindingSelections = new();
+ ///
+ /// Per-attribute DataSourceReferenceOverride values (Task 18). Mirrors
+ /// by attribute name. Loaded from the
+ /// existing rows on init; round-tripped
+ /// through on SaveBindings.
+ ///
+ private Dictionary _bindingOverrides = new();
private int _bulkConnectionId;
+ // OPC UA tag browser (Task 18) — single dialog rendered at page bottom;
+ // _browserAttrInEdit tracks which row gets the picked node id on Select.
+ private OpcUaBrowserDialog? _browserRef;
+ private string? _browserAttrInEdit;
+ private string _browserSiteIdentifier = "";
+ private string _browserConnectionName = "";
+ private string? _browserInitial;
+ private string _siteIdentifier = "";
+
+ // Test Bindings dialog — single instance, args passed via ShowAsync (no
+ // Razor parameter propagation race; same pattern as the OPC UA browser).
+ private TestBindingsDialog? _testBindingsRef;
+
// Overrides
private List _overrideAttrs = new();
private Dictionary _overrideValues = new();
@@ -407,7 +473,11 @@
_templateName = template?.Name ?? $"#{_instance.TemplateId}";
var sites = await SiteRepository.GetAllSitesAsync();
- _siteName = sites.FirstOrDefault(s => s.Id == _instance.SiteId)?.Name ?? $"#{_instance.SiteId}";
+ var site = sites.FirstOrDefault(s => s.Id == _instance.SiteId);
+ _siteName = site?.Name ?? $"#{_instance.SiteId}";
+ // Task 18: cache the site's machine identifier — the OPC UA browse
+ // dialog routes by SiteIdentifier (string), not the numeric site id.
+ _siteIdentifier = site?.SiteIdentifier ?? "";
// Areas
_siteAreas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(_instance.SiteId)).ToList();
@@ -420,7 +490,11 @@
_siteConnections = (await SiteRepository.GetDataConnectionsBySiteIdAsync(_instance.SiteId)).ToList();
var existingBindings = await TemplateEngineRepository.GetBindingsByInstanceIdAsync(Id);
foreach (var b in existingBindings)
+ {
_bindingSelections[b.AttributeName] = b.DataConnectionId;
+ if (!string.IsNullOrEmpty(b.DataSourceReferenceOverride))
+ _bindingOverrides[b.AttributeName] = b.DataSourceReferenceOverride;
+ }
// Overrides
_overrideAttrs = attrs.Where(a => !a.IsLocked).ToList();
@@ -474,12 +548,116 @@
_bindingSelections[attr.Name] = _bulkConnectionId;
}
+ // ── Task 18: per-attribute override input + OPC UA tag browser ──────────
+
+ private string? GetOverrideForAttr(string attrName)
+ => _bindingOverrides.GetValueOrDefault(attrName);
+
+ private void OnOverrideForAttrChanged(string attrName, ChangeEventArgs e)
+ {
+ var val = e.Value?.ToString();
+ if (string.IsNullOrWhiteSpace(val))
+ _bindingOverrides.Remove(attrName);
+ else
+ _bindingOverrides[attrName] = val;
+ }
+
+ /// Looks up the template default DataSourceReference for an attribute.
+ private string? GetTemplateDefault(string attrName)
+ => _bindingDataSourceAttrs.FirstOrDefault(a => a.Name == attrName)?.DataSourceReference;
+
+ /// True when the row's selected data connection is OPC UA.
+ private bool IsOpcUa(int connectionId)
+ => connectionId > 0
+ && string.Equals(
+ _siteConnections.FirstOrDefault(c => c.Id == connectionId)?.Protocol,
+ "OpcUa",
+ StringComparison.OrdinalIgnoreCase);
+
+ ///
+ /// Opens the OPC UA tag browser dialog for the given attribute row. Remembers
+ /// which attribute is being edited so can
+ /// write the picked node id back to the right override input.
+ ///
+ private async Task OpenBrowser(string attrName)
+ {
+ var connId = GetBindingConnectionId(attrName);
+ var conn = _siteConnections.FirstOrDefault(c => c.Id == connId);
+ if (conn is null) return;
+
+ _browserAttrInEdit = attrName;
+ _browserConnectionName = conn.Name;
+ _browserSiteIdentifier = _siteIdentifier;
+ _browserInitial = _bindingOverrides.GetValueOrDefault(attrName)
+ ?? GetTemplateDefault(attrName);
+
+ if (_browserRef is not null)
+ await _browserRef.ShowAsync(_siteIdentifier, conn.Name, _browserInitial);
+ }
+
+ private void OnBrowserSelected(string nodeId)
+ {
+ if (_browserAttrInEdit is null) return;
+ _bindingOverrides[_browserAttrInEdit] = nodeId;
+ _browserAttrInEdit = null;
+ }
+
+ // ── Test Bindings (one-shot live read of bound tags) ────────────────────
+
+ ///
+ /// Builds the list of testable rows: attributes that have a connection
+ /// picked AND a non-empty effective tag path AND an OPC UA connection
+ /// (the only protocol routed through ReadTagValuesCommand today).
+ ///
+ private List BuildTestableRows()
+ {
+ var rows = new List();
+ foreach (var attr in _bindingDataSourceAttrs)
+ {
+ var connId = GetBindingConnectionId(attr.Name);
+ if (connId <= 0) continue;
+
+ var conn = _siteConnections.FirstOrDefault(c => c.Id == connId);
+ if (conn is null) continue;
+
+ // OPC UA only — other protocols don't have a site-side
+ // ReadTagValuesCommand handler wired up yet.
+ if (!string.Equals(conn.Protocol, "OpcUa", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ var effectivePath = _bindingOverrides.GetValueOrDefault(attr.Name)
+ ?? GetTemplateDefault(attr.Name);
+ if (string.IsNullOrWhiteSpace(effectivePath)) continue;
+
+ rows.Add(new TestBindingsDialog.BindingRowToTest(attr.Name, conn.Name, effectivePath));
+ }
+ return rows;
+ }
+
+ private bool HasTestableBindings() => BuildTestableRows().Count > 0;
+
+ private async Task OpenTestBindings()
+ {
+ if (_testBindingsRef is null) return;
+ var rows = BuildTestableRows();
+ if (rows.Count == 0) return;
+ await _testBindingsRef.ShowAsync(_siteIdentifier, rows, _instance?.UniqueName ?? "");
+ }
+
private async Task SaveBindings()
{
_saving = true;
try
{
- var bindings = _bindingSelections.Select(kv => new ConnectionBinding(kv.Key, kv.Value)).ToList();
+ // Task 18: include the per-attribute DataSourceReferenceOverride on
+ // the wire record so it round-trips through SetConnectionBindingsAsync
+ // into the InstanceConnectionBinding entity.
+ var bindings = _bindingSelections
+ .Select(kv => new ConnectionBinding(
+ kv.Key,
+ kv.Value,
+ _bindingOverrides.GetValueOrDefault(kv.Key)))
+ .ToList();
var user = await GetCurrentUserAsync();
var result = await InstanceService.SetConnectionBindingsAsync(Id, bindings, user);
if (result.IsSuccess)
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs
index 9b8fdcb7..7db879ac 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs
@@ -50,6 +50,18 @@ public static class ServiceCollectionExtensions
// Backs the Audit Log page's Export button via GET /api/centralui/audit/export.
services.AddScoped();
+ // OPC UA Tag Browser (Task 14): facade over CommunicationService.BrowseOpcUaNodeAsync
+ // that enforces the CentralUI-side Design-role trust boundary and translates
+ // transport failures into typed BrowseFailure results for the dialog.
+ services.AddScoped();
+
+ // Test Bindings: facade over CommunicationService.ReadTagValuesAsync —
+ // same Design-role guard + typed-failure translation as the browse
+ // service. Backs the Test Bindings dialog on the Configure Instance
+ // page (one-shot live read of every bound attribute, grouped by
+ // connection).
+ services.AddScoped();
+
// Roslyn-backed C# analysis for the Monaco script editor.
// Scoped because SharedScriptCatalog wraps a scoped service.
services.AddMemoryCache(o => o.SizeLimit = 200);
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingTester.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingTester.cs
new file mode 100644
index 00000000..2c19d8b1
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingTester.cs
@@ -0,0 +1,81 @@
+using Microsoft.AspNetCore.Components.Authorization;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+using ZB.MOM.WW.ScadaBridge.Communication;
+using ZB.MOM.WW.ScadaBridge.Security;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+
+///
+/// Default implementation — a thin facade over
+/// that enforces the
+/// CentralUI-side Design-role trust boundary and translates transport
+/// exceptions into a typed result. Mirrors
+/// .
+///
+public sealed class BindingTester : IBindingTester
+{
+ private readonly CommunicationService _communication;
+ private readonly AuthenticationStateProvider _auth;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// Central-side cluster communication service.
+ /// Authentication state provider used for the Design-role guard.
+ public BindingTester(CommunicationService communication, AuthenticationStateProvider auth)
+ {
+ _communication = communication ?? throw new ArgumentNullException(nameof(communication));
+ _auth = auth ?? throw new ArgumentNullException(nameof(auth));
+ }
+
+ ///
+ public async Task ReadAsync(
+ string siteId,
+ string connectionName,
+ IReadOnlyList tagPaths,
+ CancellationToken ct = default)
+ {
+ // CentralUI-side role guard — sites don't enforce envelope-level
+ // roles, so the Design check must happen here before any cross-cluster
+ // traffic. Use HasClaim against JwtTokenService.RoleClaimType (not
+ // IsInRole, per c1e16cf).
+ var state = await _auth.GetAuthenticationStateAsync();
+ if (!state.User.HasClaim(JwtTokenService.RoleClaimType, "Design"))
+ {
+ return new ReadTagValuesResult(
+ Array.Empty(),
+ new ReadTagValuesFailure(ReadTagValuesFailureKind.ServerError, "Not authorized."));
+ }
+
+ try
+ {
+ return await _communication.ReadTagValuesAsync(
+ siteId,
+ new ReadTagValuesCommand(connectionName, tagPaths),
+ ct);
+ }
+ catch (TimeoutException ex)
+ {
+ // Akka Ask timed out — the site (or its OPC UA session) didn't
+ // answer within CommunicationOptions.QueryTimeout. Surface as a
+ // typed Timeout failure so the dialog can render an inline banner.
+ return new ReadTagValuesResult(
+ Array.Empty(),
+ new ReadTagValuesFailure(ReadTagValuesFailureKind.Timeout, ex.Message));
+ }
+ catch (OperationCanceledException)
+ {
+ // Caller-initiated cancel — propagate so Blazor can drop the
+ // response cleanly. Distinct from Timeout.
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // Any other transport / serialization failure: keep the dialog
+ // alive with a typed banner.
+ return new ReadTagValuesResult(
+ Array.Empty(),
+ new ReadTagValuesFailure(ReadTagValuesFailureKind.ServerError, ex.Message));
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IBindingTester.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IBindingTester.cs
new file mode 100644
index 00000000..91a08dde
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IBindingTester.cs
@@ -0,0 +1,39 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+
+///
+/// CentralUI facade over the central-to-site "Test Bindings" read command.
+/// Backs the Test Bindings dialog on the Configure Instance page: on open and
+/// on Refresh, the dialog issues one per distinct
+/// connection (grouped from the page's bindings table) and renders the
+/// per-tag outcomes.
+///
+///
+/// The service is the trust boundary for the read capability: it enforces the
+/// Design role at central before any cross-cluster traffic is
+/// generated, because site-side actors do not unwrap the central trust
+/// envelope. Transport failures (timeouts, unreachable sites) are translated
+/// into a typed so the dialog can render an
+/// inline banner without crashing — same shape as
+/// .
+///
+public interface IBindingTester
+{
+ ///
+ /// Reads the current value of one or more tags on the live server backing
+ /// at . The
+ /// caller is expected to group its bindings by connection name and issue
+ /// one call per group (in parallel — the dialog uses
+ /// Task.WhenAll).
+ ///
+ /// The target site identifier.
+ /// Name of the site-local data connection — the site's DataConnectionManagerActor indexes its children by name.
+ /// Tag paths to read.
+ /// Cancellation token.
+ Task ReadAsync(
+ string siteId,
+ string connectionName,
+ IReadOnlyList tagPaths,
+ CancellationToken ct = default);
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IOpcUaBrowseService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IOpcUaBrowseService.cs
new file mode 100644
index 00000000..ea9aaf0e
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IOpcUaBrowseService.cs
@@ -0,0 +1,37 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+
+///
+/// CentralUI facade over the central-to-site OPC UA browse command. Backs the
+/// OPC UA Tag Browser dialog: each tree expansion / manual node-id paste calls
+/// , which forwards a
+/// to the owning site via
+/// .
+///
+///
+/// The service is the trust boundary for the browse capability: it enforces the
+/// Design role at central before any cross-cluster traffic is generated,
+/// because site-side actors do not unwrap the central trust envelope. Transport
+/// failures (timeouts, unreachable sites) are translated into a typed
+/// so the dialog can render an inline error and
+/// remain usable (manual node-id paste still works).
+///
+public interface IOpcUaBrowseService
+{
+ ///
+ /// Enumerates the immediate children of an OPC UA node on the live server
+ /// backing at .
+ /// Pass null for to browse from the
+ /// server root (ObjectsFolder).
+ ///
+ /// The target site identifier.
+ /// Name of the site-local data connection to browse against — the site's DataConnectionManagerActor indexes its children by name.
+ /// Node to browse, or null to browse from the server root.
+ /// Cancellation token.
+ Task BrowseChildrenAsync(
+ string siteId,
+ string connectionName,
+ string? parentNodeId,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/OpcUaBrowseService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/OpcUaBrowseService.cs
new file mode 100644
index 00000000..1fb2d686
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/OpcUaBrowseService.cs
@@ -0,0 +1,89 @@
+using Microsoft.AspNetCore.Components.Authorization;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+using ZB.MOM.WW.ScadaBridge.Communication;
+using ZB.MOM.WW.ScadaBridge.Security;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+
+///
+/// Default implementation — a thin facade over
+/// that enforces the
+/// CentralUI-side Design-role trust boundary and translates transport
+/// exceptions into a typed result.
+///
+///
+/// Site-side actors (SiteCommunicationActor + DeploymentManagerActor)
+/// do not unwrap the central trust envelope, so the role check MUST run here —
+/// never on the site. Transport failures collapse into Timeout or
+/// ServerError so the dialog can show an inline banner while leaving the
+/// manual node-id paste field usable.
+///
+public sealed class OpcUaBrowseService : IOpcUaBrowseService
+{
+ private readonly CommunicationService _communication;
+ private readonly AuthenticationStateProvider _auth;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// Central-side cluster communication service.
+ /// Authentication state provider used for the Design-role guard.
+ public OpcUaBrowseService(CommunicationService communication, AuthenticationStateProvider auth)
+ {
+ _communication = communication ?? throw new ArgumentNullException(nameof(communication));
+ _auth = auth ?? throw new ArgumentNullException(nameof(auth));
+ }
+
+ ///
+ public async Task BrowseChildrenAsync(
+ string siteId,
+ string connectionName,
+ string? parentNodeId,
+ CancellationToken cancellationToken = default)
+ {
+ // CentralUI-side role guard — sites don't enforce envelope-level roles,
+ // so the Design check must happen here before any cross-cluster traffic.
+ var state = await _auth.GetAuthenticationStateAsync();
+ if (!state.User.HasClaim(JwtTokenService.RoleClaimType, "Design"))
+ {
+ return new BrowseOpcUaNodeResult(
+ Array.Empty(),
+ Truncated: false,
+ new BrowseFailure(BrowseFailureKind.ServerError, "Not authorized."));
+ }
+
+ try
+ {
+ return await _communication.BrowseOpcUaNodeAsync(
+ siteId,
+ new BrowseOpcUaNodeCommand(connectionName, parentNodeId),
+ cancellationToken);
+ }
+ catch (TimeoutException ex)
+ {
+ // Akka Ask timed out — the site (or its OPC UA session) didn't answer
+ // within CommunicationOptions.QueryTimeout. Surface as a typed
+ // Timeout failure so the dialog can render an inline banner.
+ return new BrowseOpcUaNodeResult(
+ Array.Empty(),
+ Truncated: false,
+ new BrowseFailure(BrowseFailureKind.Timeout, ex.Message));
+ }
+ catch (OperationCanceledException)
+ {
+ // Caller-initiated cancel — propagate so Blazor can drop the response
+ // cleanly. Distinct from Timeout (which the dialog renders inline).
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // Any other transport / serialization failure: keep the dialog
+ // alive and let the user fall back to manual node-id paste.
+ return new BrowseOpcUaNodeResult(
+ Array.Empty(),
+ Truncated: false,
+ new BrowseFailure(BrowseFailureKind.ServerError, ex.Message));
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceConnectionBinding.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceConnectionBinding.cs
index 125a86aa..21ca4e7d 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceConnectionBinding.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceConnectionBinding.cs
@@ -11,6 +11,14 @@ public class InstanceConnectionBinding
/// Foreign key to the data connection that provides values for this attribute.
public int DataConnectionId { get; set; }
+ ///
+ /// Optional per-instance override of the OPC UA node identifier (or other
+ /// protocol address) for this attribute. When non-null, this value replaces
+ /// the template's DataSourceReference during flattening. When null,
+ /// the template default is used.
+ ///
+ public string? DataSourceReferenceOverride { get; set; }
+
///
/// Creates a binding for the specified attribute name.
///
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs
new file mode 100644
index 00000000..ff7ae19c
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs
@@ -0,0 +1,48 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
+
+///
+/// Optional capability for an implementation
+/// that supports browsing the server's address space. Consumed only by
+/// management/UI flows (e.g. the OPC UA tag picker on the instance config
+/// page) — never by Instance Actors on the hot path.
+///
+public interface IBrowsableDataConnection
+{
+ ///
+ /// Returns the immediate children of , or
+ /// the server's root-level nodes when null.
+ ///
+ /// Node id whose children to browse, or null for the server root (OPC UA ObjectsFolder).
+ /// Cancellation token; on cancellation the implementation should throw .
+ Task BrowseChildrenAsync(
+ string? parentNodeId,
+ CancellationToken cancellationToken = default);
+}
+
+/// Child nodes returned by the server in browse order.
+/// True when the server reported more children than the per-call cap; remaining children must be discovered via manual entry.
+public record BrowseChildrenResult(
+ IReadOnlyList Children,
+ bool Truncated);
+
+/// Server-issued node identifier (e.g. "ns=2;s=Devices.Pump1.Speed").
+/// Human-readable display name from the server's DisplayName attribute.
+/// Classifies the node for UI purposes (Variable rows are selectable; Object rows are navigable).
+/// Hint so the UI can render an expand chevron without a second roundtrip.
+public record BrowseNode(
+ string NodeId,
+ string DisplayName,
+ BrowseNodeClass NodeClass,
+ bool HasChildren);
+
+public enum BrowseNodeClass { Object, Variable, Method, Other }
+
+///
+/// Thrown by when
+/// the underlying session is not currently connected. Translated to
+/// BrowseFailureKind.ConnectionNotConnected by the site-side handler.
+///
+public sealed class ConnectionNotConnectedException : InvalidOperationException
+{
+ public ConnectionNotConnectedException(string message) : base(message) { }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/BrowseCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/BrowseCommands.cs
new file mode 100644
index 00000000..73fffa58
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/BrowseCommands.cs
@@ -0,0 +1,38 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+
+///
+/// Sent from CentralUI to a specific site to enumerate the immediate children
+/// of an OPC UA node on the live server backing the given data connection.
+///
+///
+/// Keyed by (not id) because the site-side
+/// DataConnectionManagerActor indexes its children by connection name —
+/// the central UI already has the connection name in scope (dropdown), so a
+/// string carries no extra plumbing across the trust boundary. The central
+/// DataConnections table's id is intentionally not exposed at the site.
+///
+/// Name of the site-local data connection to browse against.
+/// Node to browse, or null to browse from the server root (ObjectsFolder).
+public record BrowseOpcUaNodeCommand(
+ string ConnectionName,
+ string? ParentNodeId);
+
+public record BrowseOpcUaNodeResult(
+ IReadOnlyList Children,
+ bool Truncated,
+ BrowseFailure? Failure);
+
+public record BrowseFailure(
+ BrowseFailureKind Kind,
+ string Message);
+
+public enum BrowseFailureKind
+{
+ ConnectionNotFound,
+ ConnectionNotConnected,
+ NotBrowsable,
+ Timeout,
+ ServerError
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs
index 9e13c71f..e7fcb0e9 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs
@@ -12,8 +12,17 @@ public record MgmtDeleteInstanceCommand(int InstanceId);
/// . This is a named record (not a
/// ValueTuple) so it serializes with stable, named JSON properties and can
/// evolve additively per REQ-COM-5a.
+///
+/// DataSourceReferenceOverride is an optional per-instance override of
+/// the OPC UA node id (or other protocol address) for the bound attribute.
+/// When non-null it replaces the template's DataSourceReference at
+/// flattening time; when null the template default is used.
+///
///
-public record ConnectionBinding(string AttributeName, int DataConnectionId);
+public record ConnectionBinding(
+ string AttributeName,
+ int DataConnectionId,
+ string? DataSourceReferenceOverride = null);
public record SetConnectionBindingsCommand(int InstanceId, IReadOnlyList Bindings);
public record SetInstanceOverridesCommand(int InstanceId, IReadOnlyDictionary Overrides);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ReadTagValuesCommand.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ReadTagValuesCommand.cs
new file mode 100644
index 00000000..b0416016
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ReadTagValuesCommand.cs
@@ -0,0 +1,70 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
+
+///
+/// Sent from CentralUI to a specific site to read the current value of one or
+/// more tags on the live server backing a named data connection. Backs the
+/// "Test Bindings" dialog on the Configure Instance page — a one-shot read of
+/// every bound attribute, grouped by connection, with no subscription.
+///
+///
+/// Keyed by (not id) for the same reason as
+/// : the site-side
+/// DataConnectionManagerActor indexes its children by connection name,
+/// and the central UI already has the connection name in scope from the
+/// bindings table. The central DataConnections table's id is not
+/// exposed at the site.
+///
+/// Name of the site-local data connection to read against.
+/// Tag paths to read (one batch per connection — caller groups by connection name).
+public record ReadTagValuesCommand(
+ string ConnectionName,
+ IReadOnlyList TagPaths);
+
+///
+/// Per-tag outcome of a . The site returns
+/// one outcome per requested tag path; a single failing tag never aborts the
+/// batch (the underlying IDataConnection.ReadBatchAsync contract).
+///
+/// Tag path that was read — matches an entry in the request.
+/// True when the read returned a value; false when the per-tag read failed.
+/// Read value (may be null even on success); always null on failure.
+/// Quality code as a string (Good/Bad/Uncertain); always Bad on failure.
+/// Source timestamp on success; the central-noted UTC time of the failure otherwise.
+/// Per-tag error message on failure; null on success.
+public record TagReadOutcome(
+ string TagPath,
+ bool Success,
+ object? Value,
+ string Quality,
+ DateTimeOffset Timestamp,
+ string? ErrorMessage);
+
+///
+/// Reply to a . Either
+/// is populated (one entry per requested tag, in any order) and
+/// is null, or is set and
+/// is empty — the latter is the connection-level
+/// short-circuit (unknown connection, not connected, server error, etc.) where
+/// no per-tag attempt was made.
+///
+public record ReadTagValuesResult(
+ IReadOnlyList Outcomes,
+ ReadTagValuesFailure? Failure);
+
+///
+/// Connection-level failure carried by . The
+/// dialog maps each to a friendly
+/// banner; is surfaced verbatim for the
+/// case.
+///
+public record ReadTagValuesFailure(
+ ReadTagValuesFailureKind Kind,
+ string Message);
+
+public enum ReadTagValuesFailureKind
+{
+ ConnectionNotFound,
+ ConnectionNotConnected,
+ Timeout,
+ ServerError
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs
index 728302a3..b82596d8 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs
@@ -10,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
@@ -144,6 +145,21 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
Receive(msg => _deploymentManagerProxy.Forward(msg));
Receive(msg => _deploymentManagerProxy.Forward(msg));
+ // OPC UA Tag Browser (interactive design-time query) — forward to the
+ // Deployment Manager singleton, which always lands on the active site
+ // node. Routing to the site-local /user/dcl-manager directly is wrong
+ // because the standby node has a dcl-manager too, but its
+ // DataConnectionActor children (which own the live OPC UA sessions)
+ // only exist on the singleton's node. The singleton then re-forwards
+ // to its own /user/dcl-manager, which DOES have the connection.
+ Receive(msg => _deploymentManagerProxy.Forward(msg));
+
+ // Test Bindings (interactive design-time read) — same routing rationale
+ // as BrowseOpcUaNodeCommand above: the singleton always lands on the
+ // active site node, which is the node that owns the DataConnectionActor
+ // children holding the live OPC UA sessions.
+ Receive(msg => _deploymentManagerProxy.Forward(msg));
+
// Pattern 7: Remote Queries
Receive(msg =>
{
diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
index 68bc6723..8d06e110 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
@@ -9,6 +9,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
@@ -346,6 +347,53 @@ public class CommunicationService
envelope, _options.QueryTimeout, cancellationToken);
}
+ // ── OPC UA Tag Browser (interactive design-time query) ──
+
+ ///
+ /// Asks a site to enumerate the immediate children of an OPC UA node on the
+ /// live server backing the given data connection. Used by the CentralUI OPC UA
+ /// Tag Browser dialog. The Ask is bounded by
+ /// — interactive browse expansions are short, one-shot queries that share the
+ /// same latency budget as other remote queries (event logs, parked messages).
+ ///
+ /// The target site identifier.
+ /// The OPC UA browse command.
+ /// Cancellation token.
+ /// The browse result (children + truncation flag + structured failure).
+ public Task BrowseOpcUaNodeAsync(
+ string siteId,
+ BrowseOpcUaNodeCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ var envelope = new SiteEnvelope(siteId, command);
+ return GetActor().Ask(
+ envelope, _options.QueryTimeout, cancellationToken);
+ }
+
+ // ── Test Bindings (one-shot live read of bound tags) ──
+
+ ///
+ /// Asks a site to read the current value of one or more tags on the live
+ /// server backing the given data connection. Used by the CentralUI "Test
+ /// Bindings" dialog on the Configure Instance page. The Ask is bounded by
+ /// — same latency budget
+ /// as (both are interactive one-shot
+ /// design-time queries).
+ ///
+ /// The target site identifier.
+ /// The read-tag-values command (connection name + tag paths).
+ /// Cancellation token.
+ /// The read result — per-tag outcomes plus an optional connection-level failure.
+ public Task ReadTagValuesAsync(
+ string siteId,
+ ReadTagValuesCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ var envelope = new SiteEnvelope(siteId, command);
+ return GetActor().Ask(
+ envelope, _options.QueryTimeout, cancellationToken);
+ }
+
// ── Pattern 8: Heartbeat (site→central, Tell) ──
// Heartbeats are received by central, not sent. No method needed here.
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs
index 60de88cf..bd14aeeb 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs
@@ -107,6 +107,10 @@ public class InstanceConnectionBindingConfiguration : IEntityTypeConfiguration b.DataSourceReferenceOverride)
+ .HasMaxLength(512)
+ .IsRequired(false);
+
builder.HasOne()
.WithMany()
.HasForeignKey(b => b.DataConnectionId)
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20260528155520_AddInstanceConnectionBindingOverride.Designer.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20260528155520_AddInstanceConnectionBindingOverride.Designer.cs
new file mode 100644
index 00000000..1fc7a26d
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/20260528155520_AddInstanceConnectionBindingOverride.Designer.cs
@@ -0,0 +1,1667 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
+
+#nullable disable
+
+namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
+{
+ [DbContext(typeof(ScadaBridgeDbContext))]
+ [Migration("20260528155520_AddInstanceConnectionBindingOverride")]
+ partial class AddInstanceConnectionBindingOverride
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("FriendlyName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Xml")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.AuditEvent", b =>
+ {
+ b.Property("EventId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("OccurredAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Actor")
+ .HasMaxLength(128)
+ .IsUnicode(false)
+ .HasColumnType("varchar(128)");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("CorrelationId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DurationMs")
+ .HasColumnType("int");
+
+ b.Property("ErrorDetail")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("ExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Extra")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ForwardState")
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("HttpStatus")
+ .HasColumnType("int");
+
+ b.Property("IngestedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("ParentExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PayloadTruncated")
+ .HasColumnType("bit");
+
+ b.Property("RequestSummary")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ResponseSummary")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceInstanceId")
+ .HasMaxLength(128)
+ .IsUnicode(false)
+ .HasColumnType("varchar(128)");
+
+ b.Property("SourceNode")
+ .HasMaxLength(64)
+ .IsUnicode(false)
+ .HasColumnType("varchar(64)");
+
+ b.Property("SourceScript")
+ .HasMaxLength(128)
+ .IsUnicode(false)
+ .HasColumnType("varchar(128)");
+
+ b.Property("SourceSiteId")
+ .HasMaxLength(64)
+ .IsUnicode(false)
+ .HasColumnType("varchar(64)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("Target")
+ .HasMaxLength(256)
+ .IsUnicode(false)
+ .HasColumnType("varchar(256)");
+
+ b.HasKey("EventId", "OccurredAtUtc");
+
+ b.HasIndex("CorrelationId")
+ .HasDatabaseName("IX_AuditLog_CorrelationId")
+ .HasFilter("[CorrelationId] IS NOT NULL");
+
+ b.HasIndex("EventId")
+ .IsUnique()
+ .HasDatabaseName("UX_AuditLog_EventId");
+
+ b.HasIndex("ExecutionId")
+ .HasDatabaseName("IX_AuditLog_Execution")
+ .HasFilter("[ExecutionId] IS NOT NULL");
+
+ b.HasIndex("OccurredAtUtc")
+ .IsDescending()
+ .HasDatabaseName("IX_AuditLog_OccurredAtUtc");
+
+ b.HasIndex("ParentExecutionId")
+ .HasDatabaseName("IX_AuditLog_ParentExecution")
+ .HasFilter("[ParentExecutionId] IS NOT NULL");
+
+ b.HasIndex("SourceNode", "OccurredAtUtc")
+ .HasDatabaseName("IX_AuditLog_Node_Occurred");
+
+ b.HasIndex("SourceSiteId", "OccurredAtUtc")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_AuditLog_Site_Occurred");
+
+ b.HasIndex("Target", "OccurredAtUtc")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_AuditLog_Target_Occurred")
+ .HasFilter("[Target] IS NOT NULL");
+
+ b.HasIndex("Channel", "Status", "OccurredAtUtc")
+ .IsDescending(false, false, true)
+ .HasDatabaseName("IX_AuditLog_Channel_Status_Occurred");
+
+ b.ToTable("AuditLog", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.AuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("AfterStateJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BundleImportId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EntityId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("EntityName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("User")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Action");
+
+ b.HasIndex("BundleImportId")
+ .HasDatabaseName("IX_AuditLogEntries_BundleImportId");
+
+ b.HasIndex("EntityId");
+
+ b.HasIndex("EntityType");
+
+ b.HasIndex("Timestamp");
+
+ b.HasIndex("User");
+
+ b.ToTable("AuditLogEntries");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.SiteCall", b =>
+ {
+ b.Property("TrackedOperationId")
+ .HasMaxLength(36)
+ .IsUnicode(false)
+ .HasColumnType("varchar(36)");
+
+ b.Property("Channel")
+ .IsRequired()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("HttpStatus")
+ .HasColumnType("int");
+
+ b.Property("IngestedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("LastError")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("RetryCount")
+ .HasColumnType("int");
+
+ b.Property("SourceNode")
+ .HasMaxLength(64)
+ .IsUnicode(false)
+ .HasColumnType("varchar(64)");
+
+ b.Property("SourceSite")
+ .IsRequired()
+ .HasMaxLength(64)
+ .IsUnicode(false)
+ .HasColumnType("varchar(64)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("Target")
+ .IsRequired()
+ .HasMaxLength(256)
+ .IsUnicode(false)
+ .HasColumnType("varchar(256)");
+
+ b.Property("TerminalAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.HasKey("TrackedOperationId");
+
+ b.HasIndex("SourceSite", "CreatedAtUtc")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_SiteCalls_Source_Created");
+
+ b.HasIndex("Status", "UpdatedAtUtc")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_SiteCalls_Status_Updated");
+
+ b.ToTable("SiteCalls", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeployedConfigSnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ConfigurationJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeployedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeploymentId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("InstanceId")
+ .HasColumnType("int");
+
+ b.Property("RevisionHash")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeploymentId");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DeployedConfigSnapshots");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeploymentRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeployedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeployedBy")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("DeploymentId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InstanceId")
+ .HasColumnType("int");
+
+ b.Property("RevisionHash")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeployedAt");
+
+ b.HasIndex("DeploymentId")
+ .IsUnique();
+
+ b.HasIndex("InstanceId");
+
+ b.ToTable("DeploymentRecords");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.SystemArtifactDeploymentRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ArtifactType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("DeployedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeployedBy")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("PerSiteStatus")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeployedAt");
+
+ b.ToTable("SystemArtifactDeploymentRecords");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.DatabaseConnectionDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(8000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MaxRetries")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("RetryDelay")
+ .HasColumnType("time");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("DatabaseConnectionDefinitions");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AuthConfiguration")
+ .HasMaxLength(8000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AuthType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("EndpointUrl")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("MaxRetries")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("RetryDelay")
+ .HasColumnType("time");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ExternalSystemDefinitions");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems.ExternalSystemMethod", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ExternalSystemDefinitionId")
+ .HasColumnType("int");
+
+ b.Property("HttpMethod")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ParameterDefinitions")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("ReturnDefinition")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExternalSystemDefinitionId", "Name")
+ .IsUnique();
+
+ b.ToTable("ExternalSystemMethods");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("IsEnabled")
+ .HasColumnType("bit");
+
+ b.Property("KeyHash")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("KeyHash")
+ .IsUnique();
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi.ApiMethod", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ApprovedApiKeyIds")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ParameterDefinitions")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("ReturnDefinition")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("Script")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TimeoutSeconds")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ApiMethods");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Area", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ParentAreaId")
+ .HasColumnType("int");
+
+ b.Property("SiteId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentAreaId");
+
+ b.HasIndex("SiteId", "ParentAreaId", "Name")
+ .IsUnique()
+ .HasFilter("[ParentAreaId] IS NOT NULL");
+
+ b.ToTable("Areas");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.Instance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AreaId")
+ .HasColumnType("int");
+
+ b.Property("SiteId")
+ .HasColumnType("int");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("TemplateId")
+ .HasColumnType("int");
+
+ b.Property("UniqueName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AreaId");
+
+ b.HasIndex("TemplateId");
+
+ b.HasIndex("SiteId", "UniqueName")
+ .IsUnique();
+
+ b.ToTable("Instances");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAlarmOverride", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AlarmCanonicalName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("InstanceId")
+ .HasColumnType("int");
+
+ b.Property("PriorityLevelOverride")
+ .HasColumnType("int");
+
+ b.Property("TriggerConfigurationOverride")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "AlarmCanonicalName")
+ .IsUnique();
+
+ b.ToTable("InstanceAlarmOverrides");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceAttributeOverride", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AttributeName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("InstanceId")
+ .HasColumnType("int");
+
+ b.Property("OverrideValue")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "AttributeName")
+ .IsUnique();
+
+ b.ToTable("InstanceAttributeOverrides");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances.InstanceConnectionBinding", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AttributeName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("DataConnectionId")
+ .HasColumnType("int");
+
+ b.Property("DataSourceReferenceOverride")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("InstanceId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DataConnectionId");
+
+ b.HasIndex("InstanceId", "AttributeName")
+ .IsUnique();
+
+ b.ToTable("InstanceConnectionBindings");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.Notification", b =>
+ {
+ b.Property("NotificationId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Body")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeliveredAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("LastAttemptAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("LastError")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("ListName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("NextAttemptAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("OriginExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("OriginParentExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ResolvedTargets")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RetryCount")
+ .HasColumnType("int");
+
+ b.Property("SiteEnqueuedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("SourceInstanceId")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SourceNode")
+ .HasMaxLength(64)
+ .IsUnicode(false)
+ .HasColumnType("varchar(64)");
+
+ b.Property("SourceScript")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SourceSiteId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("TypeData")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("NotificationId");
+
+ b.HasIndex("SourceSiteId", "CreatedAt");
+
+ b.HasIndex("Status", "NextAttemptAt");
+
+ b.ToTable("Notifications");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("NotificationLists");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.NotificationRecipient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EmailAddress")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("NotificationListId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NotificationListId");
+
+ b.ToTable("NotificationRecipients");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.SmtpConfiguration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AuthType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("ConnectionTimeoutSeconds")
+ .HasColumnType("int");
+
+ b.Property("Credentials")
+ .HasMaxLength(8000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FromAddress")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("MaxConcurrentConnections")
+ .HasColumnType("int");
+
+ b.Property("MaxRetries")
+ .HasColumnType("int");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("RetryDelay")
+ .HasColumnType("time");
+
+ b.Property("TlsMode")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("SmtpConfigurations");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts.SharedScript", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ParameterDefinitions")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("ReturnDefinition")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("SharedScripts");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.LdapGroupMapping", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("LdapGroupName")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LdapGroupName")
+ .IsUnique();
+
+ b.ToTable("LdapGroupMappings");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ LdapGroupName = "SCADA-Admins",
+ Role = "Admin"
+ },
+ new
+ {
+ Id = 2,
+ LdapGroupName = "SCADA-Designers",
+ Role = "Design"
+ },
+ new
+ {
+ Id = 3,
+ LdapGroupName = "SCADA-Deploy-All",
+ Role = "Deployment"
+ },
+ new
+ {
+ Id = 4,
+ LdapGroupName = "SCADA-Deploy-SiteA",
+ Role = "Deployment"
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Security.SiteScopeRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("LdapGroupMappingId")
+ .HasColumnType("int");
+
+ b.Property("SiteId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SiteId");
+
+ b.HasIndex("LdapGroupMappingId", "SiteId")
+ .IsUnique();
+
+ b.ToTable("SiteScopeRules");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.DataConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BackupConfiguration")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("FailoverRetryCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(3);
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("PrimaryConfiguration")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("Protocol")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("SiteId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SiteId", "Name")
+ .IsUnique();
+
+ b.ToTable("DataConnections");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property