diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs
new file mode 100644
index 0000000..445b84b
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs
@@ -0,0 +1,44 @@
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+///
+/// Runtime resilience counters the CapabilityInvoker + MemoryTracking + MemoryRecycle
+/// surfaces for each (DriverInstanceId, HostName) pair. Separate from
+/// (which owns per-host connectivity state) so a
+/// host that's Running but has tripped its breaker or is approaching its memory ceiling
+/// shows up distinctly on Admin /hosts.
+///
+///
+/// Per docs/v2/implementation/phase-6-1-resilience-and-observability.md §Stream E.1.
+/// The Admin UI left-joins this table on DriverHostStatus for display; rows are written
+/// by the runtime via a HostedService that samples the tracker at a configurable
+/// interval (default 5 s) — writes are non-critical, a missed sample is tolerated.
+///
+public sealed class DriverInstanceResilienceStatus
+{
+ public required string DriverInstanceId { get; set; }
+ public required string HostName { get; set; }
+
+ /// Most recent time the circuit breaker for this (instance, host) opened; null if never.
+ public DateTime? LastCircuitBreakerOpenUtc { get; set; }
+
+ /// Rolling count of consecutive Polly pipeline failures for this (instance, host).
+ public int ConsecutiveFailures { get; set; }
+
+ /// Current Polly bulkhead depth (in-flight calls) for this (instance, host).
+ public int CurrentBulkheadDepth { get; set; }
+
+ /// Most recent process recycle time (Tier C only; null for in-process tiers).
+ public DateTime? LastRecycleUtc { get; set; }
+
+ ///
+ /// Post-init memory baseline captured by MemoryTracking (median of first
+ /// BaselineWindow samples). Zero while still warming up.
+ ///
+ public long BaselineFootprintBytes { get; set; }
+
+ /// Most recent footprint sample the tracker saw (steady-state read).
+ public long CurrentFootprintBytes { get; set; }
+
+ /// Row last-write timestamp — advances on every sampling tick.
+ public DateTime LastSampledUtc { get; set; }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.Designer.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.Designer.cs
new file mode 100644
index 0000000..76a80ed
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.Designer.cs
@@ -0,0 +1,1287 @@
+//
+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.OtOpcUa.Configuration;
+
+#nullable disable
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
+{
+ [DbContext(typeof(OtOpcUaConfigDbContext))]
+ [Migration("20260419124034_AddDriverInstanceResilienceStatus")]
+ partial class AddDriverInstanceResilienceStatus
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ApplicationUri")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("DashboardPort")
+ .HasColumnType("int");
+
+ b.Property("DriverConfigOverridesJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("OpcUaPort")
+ .HasColumnType("int");
+
+ b.Property("RedundancyRole")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("ServiceLevelBase")
+ .HasColumnType("tinyint");
+
+ b.HasKey("NodeId");
+
+ b.HasIndex("ApplicationUri")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNode_ApplicationUri");
+
+ b.HasIndex("ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNode_Primary_Per_Cluster")
+ .HasFilter("[RedundancyRole] = 'Primary'");
+
+ b.ToTable("ClusterNode", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeCredential", b =>
+ {
+ b.Property("CredentialId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("NodeId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RotatedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.HasKey("CredentialId");
+
+ b.HasIndex("Kind", "Value")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNodeCredential_Value")
+ .HasFilter("[Enabled] = 1");
+
+ b.HasIndex("NodeId", "Enabled")
+ .HasDatabaseName("IX_ClusterNodeCredential_NodeId");
+
+ b.ToTable("ClusterNodeCredential", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CurrentGenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("LastAppliedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastAppliedError")
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("LastAppliedStatus")
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("NodeId");
+
+ b.HasIndex("CurrentGenerationId")
+ .HasDatabaseName("IX_ClusterNodeGenerationState_Generation");
+
+ b.ToTable("ClusterNodeGenerationState", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigAuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AuditId"));
+
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DetailsJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Principal")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Timestamp")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("GenerationId")
+ .HasDatabaseName("IX_ConfigAuditLog_Generation")
+ .HasFilter("[GenerationId] IS NOT NULL");
+
+ b.HasIndex("ClusterId", "Timestamp")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_ConfigAuditLog_Cluster_Time");
+
+ b.ToTable("ConfigAuditLog", null, t =>
+ {
+ t.HasCheckConstraint("CK_ConfigAuditLog_DetailsJson_IsJson", "DetailsJson IS NULL OR ISJSON(DetailsJson) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", b =>
+ {
+ b.Property("GenerationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GenerationId"));
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("ParentGenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("PublishedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("PublishedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.HasKey("GenerationId");
+
+ b.HasIndex("ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_ConfigGeneration_Draft_Per_Cluster")
+ .HasFilter("[Status] = 'Draft'");
+
+ b.HasIndex("ParentGenerationId");
+
+ b.HasIndex("ClusterId", "Status", "GenerationId")
+ .IsDescending(false, false, true)
+ .HasDatabaseName("IX_ConfigGeneration_Cluster_Published");
+
+ SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("ClusterId", "Status", "GenerationId"), new[] { "PublishedAt" });
+
+ b.ToTable("ConfigGeneration", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Device", b =>
+ {
+ b.Property("DeviceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DeviceConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.HasKey("DeviceRowId");
+
+ b.HasIndex("GenerationId", "DeviceId")
+ .IsUnique()
+ .HasDatabaseName("UX_Device_Generation_LogicalId")
+ .HasFilter("[DeviceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_Device_Generation_Driver");
+
+ b.ToTable("Device", null, t =>
+ {
+ t.HasCheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverHostStatus", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HostName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Detail")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("LastSeenUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("StateChangedUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("NodeId", "DriverInstanceId", "HostName");
+
+ b.HasIndex("LastSeenUtc")
+ .HasDatabaseName("IX_DriverHostStatus_LastSeen");
+
+ b.HasIndex("NodeId")
+ .HasDatabaseName("IX_DriverHostStatus_Node");
+
+ b.ToTable("DriverHostStatus", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance", b =>
+ {
+ b.Property("DriverInstanceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("NamespaceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("DriverInstanceRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_DriverInstance_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .IsUnique()
+ .HasDatabaseName("UX_DriverInstance_Generation_LogicalId")
+ .HasFilter("[DriverInstanceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "NamespaceId")
+ .HasDatabaseName("IX_DriverInstance_Generation_Namespace");
+
+ b.ToTable("DriverInstance", null, t =>
+ {
+ t.HasCheckConstraint("CK_DriverInstance_DriverConfig_IsJson", "ISJSON(DriverConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstanceResilienceStatus", b =>
+ {
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HostName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("BaselineFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("int");
+
+ b.Property("CurrentBulkheadDepth")
+ .HasColumnType("int");
+
+ b.Property("CurrentFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("LastCircuitBreakerOpenUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastRecycleUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastSampledUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("DriverInstanceId", "HostName");
+
+ b.HasIndex("LastSampledUtc")
+ .HasDatabaseName("IX_DriverResilience_LastSampled");
+
+ b.ToTable("DriverInstanceResilienceStatus", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
+ {
+ b.Property("EquipmentRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("AssetLocation")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DeviceManualUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("EquipmentClassRef")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("EquipmentId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentUuid")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("HardwareRevision")
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("MachineCode")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Manufacturer")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ManufacturerUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Model")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("SAPID")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SerialNumber")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SoftwareRevision")
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("UnsLineId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("YearOfConstruction")
+ .HasColumnType("smallint");
+
+ b.Property("ZTag")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("EquipmentRowId");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_Equipment_Generation_Driver");
+
+ b.HasIndex("GenerationId", "EquipmentId")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_LogicalId")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "EquipmentUuid")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_Uuid");
+
+ b.HasIndex("GenerationId", "MachineCode")
+ .HasDatabaseName("IX_Equipment_Generation_MachineCode");
+
+ b.HasIndex("GenerationId", "SAPID")
+ .HasDatabaseName("IX_Equipment_Generation_SAPID")
+ .HasFilter("[SAPID] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsLineId")
+ .HasDatabaseName("IX_Equipment_Generation_Line");
+
+ b.HasIndex("GenerationId", "ZTag")
+ .HasDatabaseName("IX_Equipment_Generation_ZTag")
+ .HasFilter("[ZTag] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsLineId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_LinePath");
+
+ b.ToTable("Equipment", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ExternalIdReservation", b =>
+ {
+ b.Property("ReservationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentUuid")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("FirstPublishedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("FirstPublishedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("LastPublishedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("ReleaseReason")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("ReleasedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ReleasedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("ReservationId");
+
+ b.HasIndex("EquipmentUuid")
+ .HasDatabaseName("IX_ExternalIdReservation_Equipment");
+
+ b.HasIndex("Kind", "Value")
+ .IsUnique()
+ .HasDatabaseName("UX_ExternalIdReservation_KindValue_Active")
+ .HasFilter("[ReleasedAt] IS NULL");
+
+ b.ToTable("ExternalIdReservation", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
+ {
+ b.Property("NamespaceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("NamespaceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("NamespaceUri")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.HasKey("NamespaceRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_Namespace_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "NamespaceId")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_LogicalId")
+ .HasFilter("[NamespaceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "NamespaceUri")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_NamespaceUri");
+
+ b.HasIndex("GenerationId", "ClusterId", "Kind")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_Cluster_Kind");
+
+ b.HasIndex("GenerationId", "NamespaceId", "ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_LogicalId_Cluster")
+ .HasFilter("[NamespaceId] IS NOT NULL");
+
+ b.ToTable("Namespace", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
+ {
+ b.Property("NodeAclRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("LdapGroup")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NodeAclId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("PermissionFlags")
+ .HasColumnType("int");
+
+ b.Property("ScopeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ScopeKind")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.HasKey("NodeAclRowId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_NodeAcl_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "LdapGroup")
+ .HasDatabaseName("IX_NodeAcl_Generation_Group");
+
+ b.HasIndex("GenerationId", "NodeAclId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_Generation_LogicalId")
+ .HasFilter("[NodeAclId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ScopeKind", "ScopeId")
+ .HasDatabaseName("IX_NodeAcl_Generation_Scope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ClusterId", "LdapGroup", "ScopeKind", "ScopeId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_Generation_GroupScope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.ToTable("NodeAcl", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.PollGroup", b =>
+ {
+ b.Property("PollGroupRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("IntervalMs")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("PollGroupRowId");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_PollGroup_Generation_Driver");
+
+ b.HasIndex("GenerationId", "PollGroupId")
+ .IsUnique()
+ .HasDatabaseName("UX_PollGroup_Generation_LogicalId")
+ .HasFilter("[PollGroupId] IS NOT NULL");
+
+ b.ToTable("PollGroup", null, t =>
+ {
+ t.HasCheckConstraint("CK_PollGroup_IntervalMs_Min", "IntervalMs >= 50");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
+ {
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Enterprise")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ModifiedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("NodeCount")
+ .HasColumnType("tinyint");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("RedundancyMode")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("Site")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.HasKey("ClusterId");
+
+ b.HasIndex("Name")
+ .IsUnique()
+ .HasDatabaseName("UX_ServerCluster_Name");
+
+ b.HasIndex("Site")
+ .HasDatabaseName("IX_ServerCluster_Site");
+
+ b.ToTable("ServerCluster", null, t =>
+ {
+ t.HasCheckConstraint("CK_ServerCluster_RedundancyMode_NodeCount", "((NodeCount = 1 AND RedundancyMode = 'None') OR (NodeCount = 2 AND RedundancyMode IN ('Warm', 'Hot')))");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
+ {
+ b.Property("TagRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("AccessLevel")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("DataType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("FolderPath")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("TagConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TagId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("WriteIdempotent")
+ .HasColumnType("bit");
+
+ b.HasKey("TagRowId");
+
+ b.HasIndex("GenerationId", "EquipmentId")
+ .HasDatabaseName("IX_Tag_Generation_Equipment")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "TagId")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_LogicalId")
+ .HasFilter("[TagId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId", "DeviceId")
+ .HasDatabaseName("IX_Tag_Generation_Driver_Device");
+
+ b.HasIndex("GenerationId", "EquipmentId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_EquipmentPath")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId", "FolderPath", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_FolderPath")
+ .HasFilter("[EquipmentId] IS NULL");
+
+ b.ToTable("Tag", null, t =>
+ {
+ t.HasCheckConstraint("CK_Tag_TagConfig_IsJson", "ISJSON(TagConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
+ {
+ b.Property("UnsAreaRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("UnsAreaId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsAreaRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_UnsArea_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "UnsAreaId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_Generation_LogicalId")
+ .HasFilter("[UnsAreaId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ClusterId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_Generation_ClusterName");
+
+ b.ToTable("UnsArea", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsLine", b =>
+ {
+ b.Property("UnsLineRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("UnsAreaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsLineId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsLineRowId");
+
+ b.HasIndex("GenerationId", "UnsAreaId")
+ .HasDatabaseName("IX_UnsLine_Generation_Area");
+
+ b.HasIndex("GenerationId", "UnsLineId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_Generation_LogicalId")
+ .HasFilter("[UnsLineId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsAreaId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_Generation_AreaName");
+
+ b.ToTable("UnsLine", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Nodes")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeCredential", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", "Node")
+ .WithMany("Credentials")
+ .HasForeignKey("NodeId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "CurrentGeneration")
+ .WithMany()
+ .HasForeignKey("CurrentGenerationId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", "Node")
+ .WithOne("GenerationState")
+ .HasForeignKey("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", "NodeId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("CurrentGeneration");
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Generations")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Parent")
+ .WithMany()
+ .HasForeignKey("ParentGenerationId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Device", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Namespaces")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.PollGroup", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsLine", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Navigation("Credentials");
+
+ b.Navigation("GenerationState");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
+ {
+ b.Navigation("Generations");
+
+ b.Navigation("Namespaces");
+
+ b.Navigation("Nodes");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.cs
new file mode 100644
index 0000000..120866a
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419124034_AddDriverInstanceResilienceStatus.cs
@@ -0,0 +1,46 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
+{
+ ///
+ public partial class AddDriverInstanceResilienceStatus : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "DriverInstanceResilienceStatus",
+ columns: table => new
+ {
+ DriverInstanceId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ HostName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false),
+ LastCircuitBreakerOpenUtc = table.Column(type: "datetime2(3)", nullable: true),
+ ConsecutiveFailures = table.Column(type: "int", nullable: false),
+ CurrentBulkheadDepth = table.Column(type: "int", nullable: false),
+ LastRecycleUtc = table.Column(type: "datetime2(3)", nullable: true),
+ BaselineFootprintBytes = table.Column(type: "bigint", nullable: false),
+ CurrentFootprintBytes = table.Column(type: "bigint", nullable: false),
+ LastSampledUtc = table.Column(type: "datetime2(3)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_DriverInstanceResilienceStatus", x => new { x.DriverInstanceId, x.HostName });
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DriverResilience_LastSampled",
+ table: "DriverInstanceResilienceStatus",
+ column: "LastSampledUtc");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "DriverInstanceResilienceStatus");
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
index 9b5b8f1..e634faf 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
@@ -434,6 +434,45 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
});
});
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstanceResilienceStatus", b =>
+ {
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HostName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("BaselineFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("int");
+
+ b.Property("CurrentBulkheadDepth")
+ .HasColumnType("int");
+
+ b.Property("CurrentFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("LastCircuitBreakerOpenUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastRecycleUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastSampledUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("DriverInstanceId", "HostName");
+
+ b.HasIndex("LastSampledUtc")
+ .HasDatabaseName("IX_DriverResilience_LastSampled");
+
+ b.ToTable("DriverInstanceResilienceStatus", (string)null);
+ });
+
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
{
b.Property("EquipmentRowId")
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
index 914b3dd..19414bc 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
@@ -28,6 +28,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions ConfigAuditLogs => Set();
public DbSet ExternalIdReservations => Set();
public DbSet DriverHostStatuses => Set();
+ public DbSet DriverInstanceResilienceStatuses => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -49,6 +50,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.LastSeenUtc).HasDatabaseName("IX_DriverHostStatus_LastSeen");
});
}
+
+ private static void ConfigureDriverInstanceResilienceStatus(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(e =>
+ {
+ e.ToTable("DriverInstanceResilienceStatus");
+ e.HasKey(x => new { x.DriverInstanceId, x.HostName });
+ e.Property(x => x.DriverInstanceId).HasMaxLength(64);
+ e.Property(x => x.HostName).HasMaxLength(256);
+ e.Property(x => x.LastCircuitBreakerOpenUtc).HasColumnType("datetime2(3)");
+ e.Property(x => x.LastRecycleUtc).HasColumnType("datetime2(3)");
+ e.Property(x => x.LastSampledUtc).HasColumnType("datetime2(3)");
+ // LastSampledUtc drives the Admin UI's stale-sample filter same way DriverHostStatus's
+ // LastSeenUtc index does for connectivity rows.
+ e.HasIndex(x => x.LastSampledUtc).HasDatabaseName("IX_DriverResilience_LastSampled");
+ });
+ }
}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs b/src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs
new file mode 100644
index 0000000..be4cc07
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs
@@ -0,0 +1,104 @@
+using System.Collections.Concurrent;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;
+
+///
+/// Process-singleton tracker of live resilience counters per
+/// (DriverInstanceId, HostName). Populated by the CapabilityInvoker and the
+/// MemoryTracking layer; consumed by a HostedService that periodically persists a
+/// snapshot to the DriverInstanceResilienceStatus table for Admin /hosts.
+///
+///
+/// Per Phase 6.1 Stream E. No DB dependency here — the tracker is pure in-memory so
+/// tests can exercise it without EF Core or SQL Server. The HostedService that writes
+/// snapshots lives in the Server project (Stream E.2); the actual SignalR push + Blazor
+/// page refresh (E.3) lands in a follow-up visual-review PR.
+///
+public sealed class DriverResilienceStatusTracker
+{
+ private readonly ConcurrentDictionary _status = new();
+
+ /// Record a Polly pipeline failure for .
+ public void RecordFailure(string driverInstanceId, string hostName, DateTime utcNow)
+ {
+ var key = new StatusKey(driverInstanceId, hostName);
+ _status.AddOrUpdate(key,
+ _ => new ResilienceStatusSnapshot { ConsecutiveFailures = 1, LastSampledUtc = utcNow },
+ (_, existing) => existing with
+ {
+ ConsecutiveFailures = existing.ConsecutiveFailures + 1,
+ LastSampledUtc = utcNow,
+ });
+ }
+
+ /// Reset the consecutive-failure count on a successful pipeline execution.
+ public void RecordSuccess(string driverInstanceId, string hostName, DateTime utcNow)
+ {
+ var key = new StatusKey(driverInstanceId, hostName);
+ _status.AddOrUpdate(key,
+ _ => new ResilienceStatusSnapshot { ConsecutiveFailures = 0, LastSampledUtc = utcNow },
+ (_, existing) => existing with
+ {
+ ConsecutiveFailures = 0,
+ LastSampledUtc = utcNow,
+ });
+ }
+
+ /// Record a circuit-breaker open event.
+ public void RecordBreakerOpen(string driverInstanceId, string hostName, DateTime utcNow)
+ {
+ var key = new StatusKey(driverInstanceId, hostName);
+ _status.AddOrUpdate(key,
+ _ => new ResilienceStatusSnapshot { LastBreakerOpenUtc = utcNow, LastSampledUtc = utcNow },
+ (_, existing) => existing with { LastBreakerOpenUtc = utcNow, LastSampledUtc = utcNow });
+ }
+
+ /// Record a process recycle event (Tier C only).
+ public void RecordRecycle(string driverInstanceId, string hostName, DateTime utcNow)
+ {
+ var key = new StatusKey(driverInstanceId, hostName);
+ _status.AddOrUpdate(key,
+ _ => new ResilienceStatusSnapshot { LastRecycleUtc = utcNow, LastSampledUtc = utcNow },
+ (_, existing) => existing with { LastRecycleUtc = utcNow, LastSampledUtc = utcNow });
+ }
+
+ /// Capture / update the MemoryTracking-supplied baseline + current footprint.
+ public void RecordFootprint(string driverInstanceId, string hostName, long baselineBytes, long currentBytes, DateTime utcNow)
+ {
+ var key = new StatusKey(driverInstanceId, hostName);
+ _status.AddOrUpdate(key,
+ _ => new ResilienceStatusSnapshot
+ {
+ BaselineFootprintBytes = baselineBytes,
+ CurrentFootprintBytes = currentBytes,
+ LastSampledUtc = utcNow,
+ },
+ (_, existing) => existing with
+ {
+ BaselineFootprintBytes = baselineBytes,
+ CurrentFootprintBytes = currentBytes,
+ LastSampledUtc = utcNow,
+ });
+ }
+
+ /// Snapshot of a specific (instance, host) pair; null if no counters recorded yet.
+ public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) =>
+ _status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null;
+
+ /// Copy of every currently-tracked (instance, host, snapshot) triple. Safe under concurrent writes.
+ public IReadOnlyList<(string DriverInstanceId, string HostName, ResilienceStatusSnapshot Snapshot)> Snapshot() =>
+ _status.Select(kvp => (kvp.Key.DriverInstanceId, kvp.Key.HostName, kvp.Value)).ToList();
+
+ private readonly record struct StatusKey(string DriverInstanceId, string HostName);
+}
+
+/// Snapshot of the resilience counters for one (DriverInstanceId, HostName) pair.
+public sealed record ResilienceStatusSnapshot
+{
+ public int ConsecutiveFailures { get; init; }
+ public DateTime? LastBreakerOpenUtc { get; init; }
+ public DateTime? LastRecycleUtc { get; init; }
+ public long BaselineFootprintBytes { get; init; }
+ public long CurrentFootprintBytes { get; init; }
+ public DateTime LastSampledUtc { get; init; }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs
index 68da688..d05c437 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs
@@ -29,6 +29,7 @@ public sealed class SchemaComplianceTests
"DriverInstance", "Device", "Equipment", "Tag", "PollGroup",
"NodeAcl", "ExternalIdReservation",
"DriverHostStatus",
+ "DriverInstanceResilienceStatus",
};
var actual = QueryStrings(@"
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceStatusTrackerTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceStatusTrackerTests.cs
new file mode 100644
index 0000000..adee487
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceStatusTrackerTests.cs
@@ -0,0 +1,110 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Resilience;
+
+namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
+
+[Trait("Category", "Unit")]
+public sealed class DriverResilienceStatusTrackerTests
+{
+ private static readonly DateTime Now = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc);
+
+ [Fact]
+ public void TryGet_Returns_Null_Before_AnyWrite()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.TryGet("drv", "host").ShouldBeNull();
+ }
+
+ [Fact]
+ public void RecordFailure_Accumulates_ConsecutiveFailures()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.RecordFailure("drv", "host", Now);
+ tracker.RecordFailure("drv", "host", Now.AddSeconds(1));
+ tracker.RecordFailure("drv", "host", Now.AddSeconds(2));
+
+ tracker.TryGet("drv", "host")!.ConsecutiveFailures.ShouldBe(3);
+ }
+
+ [Fact]
+ public void RecordSuccess_Resets_ConsecutiveFailures()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+ tracker.RecordFailure("drv", "host", Now);
+ tracker.RecordFailure("drv", "host", Now.AddSeconds(1));
+
+ tracker.RecordSuccess("drv", "host", Now.AddSeconds(2));
+
+ tracker.TryGet("drv", "host")!.ConsecutiveFailures.ShouldBe(0);
+ }
+
+ [Fact]
+ public void RecordBreakerOpen_Populates_LastBreakerOpenUtc()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.RecordBreakerOpen("drv", "host", Now);
+
+ tracker.TryGet("drv", "host")!.LastBreakerOpenUtc.ShouldBe(Now);
+ }
+
+ [Fact]
+ public void RecordRecycle_Populates_LastRecycleUtc()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.RecordRecycle("drv", "host", Now);
+
+ tracker.TryGet("drv", "host")!.LastRecycleUtc.ShouldBe(Now);
+ }
+
+ [Fact]
+ public void RecordFootprint_CapturesBaselineAndCurrent()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.RecordFootprint("drv", "host", baselineBytes: 100_000_000, currentBytes: 150_000_000, Now);
+
+ var snap = tracker.TryGet("drv", "host")!;
+ snap.BaselineFootprintBytes.ShouldBe(100_000_000);
+ snap.CurrentFootprintBytes.ShouldBe(150_000_000);
+ }
+
+ [Fact]
+ public void DifferentHosts_AreIndependent()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+
+ tracker.RecordFailure("drv", "host-a", Now);
+ tracker.RecordFailure("drv", "host-b", Now);
+ tracker.RecordSuccess("drv", "host-a", Now.AddSeconds(1));
+
+ tracker.TryGet("drv", "host-a")!.ConsecutiveFailures.ShouldBe(0);
+ tracker.TryGet("drv", "host-b")!.ConsecutiveFailures.ShouldBe(1);
+ }
+
+ [Fact]
+ public void Snapshot_ReturnsAll_TrackedPairs()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+ tracker.RecordFailure("drv-1", "host-a", Now);
+ tracker.RecordFailure("drv-1", "host-b", Now);
+ tracker.RecordFailure("drv-2", "host-a", Now);
+
+ var snapshot = tracker.Snapshot();
+
+ snapshot.Count.ShouldBe(3);
+ }
+
+ [Fact]
+ public void ConcurrentWrites_DoNotLose_Failures()
+ {
+ var tracker = new DriverResilienceStatusTracker();
+ Parallel.For(0, 500, _ => tracker.RecordFailure("drv", "host", Now));
+
+ tracker.TryGet("drv", "host")!.ConsecutiveFailures.ShouldBe(500);
+ }
+}