diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8687080..35ccaa6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,18 +54,18 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
+
+
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs
new file mode 100644
index 0000000..237fcb1
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigEdit.cs
@@ -0,0 +1,27 @@
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+///
+/// Append-only audit row written by AdminOperationsActor on every mutating live-edit
+/// operation. The ExecutionId optionally correlates a sequence of edits that ran inside one
+/// admin transaction (e.g. an import batch that updates many Equipment rows).
+///
+public sealed class ConfigEdit
+{
+ public Guid EditId { get; init; } = Guid.NewGuid();
+
+ public required string EntityType { get; init; }
+
+ public Guid EntityId { get; init; }
+
+ /// JSON payload of the column-name → new-value pairs touched by this edit.
+ public required string FieldsJson { get; init; }
+
+ /// Optional correlation across edits inside a single admin operation.
+ public Guid? ExecutionId { get; init; }
+
+ public required string EditedBy { get; init; }
+
+ public DateTime EditedAtUtc { get; init; } = DateTime.UtcNow;
+
+ public required string SourceNode { get; init; }
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs
new file mode 100644
index 0000000..6e618a5
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Deployment.cs
@@ -0,0 +1,30 @@
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+///
+/// Immutable snapshot of a config artifact dispatched to every driver-role node by the
+/// ConfigPublishCoordinator. Replaces the v1 draft/publish
+/// row; the ArtifactBlob carries the SnapshotAndFlatten() output produced by
+/// AdminOperationsActor.
+///
+public sealed class Deployment
+{
+ public Guid DeploymentId { get; init; } = Guid.NewGuid();
+
+ public required string RevisionHash { get; init; }
+
+ public DeploymentStatus Status { get; set; } = DeploymentStatus.Dispatching;
+
+ public required string CreatedBy { get; init; }
+
+ public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow;
+
+ public byte[] ArtifactBlob { get; init; } = Array.Empty();
+
+ public byte[] RowVersion { get; set; } = Array.Empty();
+
+ public string? FailureReason { get; set; }
+
+ public DateTime? SealedAtUtc { get; set; }
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs
new file mode 100644
index 0000000..df20587
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeDeploymentState.cs
@@ -0,0 +1,29 @@
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+///
+/// Per-(node, deployment) apply progress row owned by the DriverHostActor. Replaces the
+/// v1 single-row-per-node model with a history
+/// of every apply attempt so the ConfigPublishCoordinator can reconstruct in-flight state
+/// after a failover.
+///
+public sealed class NodeDeploymentState
+{
+ public required string NodeId { get; init; }
+
+ public Guid DeploymentId { get; init; }
+
+ public NodeDeploymentStatus Status { get; set; } = NodeDeploymentStatus.Applying;
+
+ public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
+
+ public DateTime? AppliedAtUtc { get; set; }
+
+ public string? FailureReason { get; set; }
+
+ public byte[] RowVersion { get; set; } = Array.Empty();
+
+ public ClusterNode? Node { get; set; }
+ public Deployment? Deployment { get; set; }
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs
new file mode 100644
index 0000000..e268048
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/DeploymentStatus.cs
@@ -0,0 +1,15 @@
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+///
+/// Lifecycle of a deployment artifact dispatched by the v2 ConfigPublishCoordinator.
+/// Replaces the v1 ConfigGeneration draft/publish lifecycle (decision tracked in the
+/// v2 hosting-alignment design doc).
+///
+public enum DeploymentStatus
+{
+ Dispatching = 0,
+ AwaitingApplyAcks = 1,
+ Sealed = 2,
+ PartiallyFailed = 3,
+ TimedOut = 4,
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs
new file mode 100644
index 0000000..1ce5de6
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodeDeploymentStatus.cs
@@ -0,0 +1,12 @@
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+///
+/// Per-node deployment apply state. Replaces the v1 NodeApplyStatus that was attached to
+/// ClusterNodeGenerationState.
+///
+public enum NodeDeploymentStatus
+{
+ Applying = 0,
+ Applied = 1,
+ Failed = 2,
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
index 9862de8..cba2bec 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
@@ -9,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration;
/// any divergence is a defect caught by the SchemaComplianceTests introspection check.
///
public sealed class OtOpcUaConfigDbContext(DbContextOptions options)
- : DbContext(options)
+ : DbContext(options), IDataProtectionKeyContext
{
public DbSet ServerClusters => Set();
public DbSet ClusterNodes => Set();
@@ -37,6 +38,16 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions ScriptedAlarms => Set();
public DbSet ScriptedAlarmStates => Set();
+ // v2 deploy-model tables (Phase 1 of the Akka + fused-hosting alignment). Replace the
+ // ConfigGeneration/ClusterNodeGenerationState pair when Task 14's migration runs.
+ public DbSet Deployments => Set();
+ public DbSet NodeDeploymentStates => Set();
+ public DbSet ConfigEdits => Set();
+
+ // ASP.NET DataProtection key ring storage (decision: keys persisted in ConfigDb so every
+ // admin-role node decrypts the same cookies without sharing a filesystem).
+ public DbSet DataProtectionKeys => Set();
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@@ -64,6 +75,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.UpdatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
});
}
+
+ private static void ConfigureDeployment(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(e =>
+ {
+ e.ToTable("Deployment");
+ e.HasKey(x => x.DeploymentId);
+ e.Property(x => x.DeploymentId).HasDefaultValueSql("NEWSEQUENTIALID()");
+ e.Property(x => x.RevisionHash).HasMaxLength(64).IsRequired();
+ e.Property(x => x.Status).HasConversion();
+ e.Property(x => x.CreatedBy).HasMaxLength(128).IsRequired();
+ e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
+ e.Property(x => x.ArtifactBlob).HasColumnType("varbinary(max)");
+ e.Property(x => x.RowVersion).IsRowVersion();
+ e.Property(x => x.FailureReason).HasMaxLength(2048);
+ e.Property(x => x.SealedAtUtc).HasColumnType("datetime2(3)");
+
+ e.HasIndex(x => x.Status).HasDatabaseName("IX_Deployment_Status");
+ e.HasIndex(x => x.CreatedAtUtc).HasDatabaseName("IX_Deployment_CreatedAt");
+ });
+ }
+
+ private static void ConfigureNodeDeploymentState(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(e =>
+ {
+ e.ToTable("NodeDeploymentState");
+ e.HasKey(x => new { x.NodeId, x.DeploymentId });
+ e.Property(x => x.NodeId).HasMaxLength(64);
+ e.Property(x => x.Status).HasConversion();
+ e.Property(x => x.StartedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
+ e.Property(x => x.AppliedAtUtc).HasColumnType("datetime2(3)");
+ e.Property(x => x.FailureReason).HasMaxLength(2048);
+ e.Property(x => x.RowVersion).IsRowVersion();
+
+ e.HasOne(x => x.Node).WithMany().HasForeignKey(x => x.NodeId).OnDelete(DeleteBehavior.Restrict);
+ e.HasOne(x => x.Deployment).WithMany().HasForeignKey(x => x.DeploymentId).OnDelete(DeleteBehavior.Cascade);
+
+ e.HasIndex(x => x.DeploymentId).HasDatabaseName("IX_NodeDeploymentState_Deployment");
+ e.HasIndex(x => x.Status).HasDatabaseName("IX_NodeDeploymentState_Status");
+ });
+ }
+
+ private static void ConfigureConfigEdit(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(e =>
+ {
+ e.ToTable("ConfigEdit", t =>
+ {
+ t.HasCheckConstraint("CK_ConfigEdit_FieldsJson_IsJson", "ISJSON(FieldsJson) = 1");
+ });
+ e.HasKey(x => x.EditId);
+ e.Property(x => x.EditId).HasDefaultValueSql("NEWSEQUENTIALID()");
+ e.Property(x => x.EntityType).HasMaxLength(64).IsRequired();
+ e.Property(x => x.FieldsJson).HasColumnType("nvarchar(max)").IsRequired();
+ e.Property(x => x.EditedBy).HasMaxLength(128).IsRequired();
+ e.Property(x => x.EditedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
+ e.Property(x => x.SourceNode).HasMaxLength(64).IsRequired();
+
+ // Replays of admin operations group rows by ExecutionId, then by time.
+ e.HasIndex(x => new { x.EntityType, x.EntityId }).HasDatabaseName("IX_ConfigEdit_Entity");
+ e.HasIndex(x => x.ExecutionId).HasFilter("[ExecutionId] IS NOT NULL").HasDatabaseName("IX_ConfigEdit_Execution");
+ e.HasIndex(x => x.EditedAtUtc).HasDatabaseName("IX_ConfigEdit_EditedAt");
+ });
+ }
+
+ private static void ConfigureDataProtectionKey(ModelBuilder modelBuilder)
+ {
+ // ASP.NET DataProtection ships its own EF mapping; override only the table name so it
+ // matches the rest of the schema's PascalCase convention.
+ modelBuilder.Entity(e =>
+ {
+ e.ToTable("DataProtectionKeys");
+ });
+ }
}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj
index ad38f5e..db15f31 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj
@@ -18,6 +18,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+