diff --git a/CLAUDE.md b/CLAUDE.md
index ce0fd80b..d585b07b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -232,7 +232,7 @@ Assert the **effective** cluster config off a running `ActorSystem` (as `SplitBr
**Phase 1 landed 2026-07-22 and changed a deploy-path behaviour.** `ConfigPublishCoordinator` sources its expected-ack set from **enabled `ClusterNode` rows**, not from `Akka.Cluster.State.Members` filtered by the `driver` role — central must be able to name a deployment's nodes without sharing a gossip ring with them. Consequences worth knowing before touching deploys:
-- **A configured node that is switched off now FAILS the deployment** at the apply deadline instead of letting it seal green without that node. `ClusterNode.Enabled = 0` is the maintenance hatch; the deadline log names the silent nodes.
+- **A configured node that is switched off now FAILS the deployment** at the apply deadline instead of letting it seal green without that node. The maintenance hatch is the **new `ClusterNode.MaintenanceMode` column**, NOT `Enabled = 0`: the live gate showed `Enabled = 0` is rejected outright by `DraftValidator.ValidateClusterTopology` (`ClusterEnabledNodeCountMismatch` — enabled count must equal `ServerCluster.NodeCount`), so on a 2-node pair — every cluster in the target topology — it can never be the hatch. `Enabled` = part of the declared topology; `MaintenanceMode` = do not expect it now. The deadline log names the silent nodes and points at the right flag.
- **Every `ClusterNode` row is now *defined* to be a driver node.** The DB has no per-node role column and deliberately did not gain one (it would drift from `Cluster:Roles`). Giving an admin-only node a `ClusterNode` row makes every deploy time out.
- **There is no such thing as a cluster-scoped deployment.** `Deployment` has no `ClusterId`, `ConfigComposer` always snapshots the whole DB, and `DeploymentArtifact.ResolveClusterScope` is *node-side* self-scoping of a fleet-wide artifact. The phase-1 plan asked for cluster-scope filtering of the ack set; it was dropped as describing a feature that does not exist.
- `ClusterNode` gained `AkkaPort` (NOT NULL, 4053) + `GrpcPort` (nullable) as **central's dial targets** for Phases 2/5. They duplicate the node's own `Cluster:Port` / `Cluster:PublicHostname` with no schema-level enforcement, so `ClusterNodeAddressReconcilerActor` (admin singleton) logs an Error on drift. **Phase 2 must revisit it** — once the meshes split, an admin node cannot see site members and every site row would report `EnabledRowNotInCluster` forever.
diff --git a/docs/plans/2026-07-21-per-cluster-mesh-phase1.md b/docs/plans/2026-07-21-per-cluster-mesh-phase1.md
index 1c749f87..f5b078ca 100644
--- a/docs/plans/2026-07-21-per-cluster-mesh-phase1.md
+++ b/docs/plans/2026-07-21-per-cluster-mesh-phase1.md
@@ -12,8 +12,17 @@ sourcing its expected-ack set from those rows.
**Architecture:** Additive schema change plus one substitution inside the coordinator. Nothing else
moves; the deploy channel stays on DistributedPubSub until Phase 2.
-**Status:** NOT STARTED. Read §"The decision this phase forces" before executing — it changes
-deploy-seal semantics and should be confirmed, not assumed.
+**Status: DONE 2026-07-22** (branch `feat/mesh-phase1`). Live gate:
+[`2026-07-22-mesh-phase1-live-gate.md`](2026-07-22-mesh-phase1-live-gate.md) — **PASSED**, after gate
+step 4 found that the escape hatch this phase depends on did not exist. Three deviations from the
+plan below, all decided before implementing or forced by the gate:
+
+1. **Task 3's cluster-scope filtering was dropped** — there is no cluster-scoped deployment to filter
+ on. See the gate doc, step 2.
+2. **No per-node role column.** Every `ClusterNode` row is now *defined* to be a driver node.
+3. **Task 7 added:** `ClusterNode.MaintenanceMode`. `Enabled = 0` is rejected by
+ `DraftValidator.ValidateClusterTopology` on any 2-node pair, so it could never be the maintenance
+ hatch — which the plan called "the check that makes step 3 acceptable to ship".
---
@@ -54,7 +63,7 @@ But it is a behaviour change, and it needs an operator escape hatch or a node ta
maintenance blocks every deployment. `ClusterNode.Enabled` already exists and is exactly that hatch,
so the query filters on it: **`Enabled = 1` rows are expected to ack; disabled rows are not.**
-**Confirm this before executing Task 3.** If the preference is to keep sealing green past a down
+**Confirmed 2026-07-22 before executing Task 3** — DB-derived with the hatch. (The hatch turned out to be `MaintenanceMode`, not `Enabled`; see Task 7.) If the preference is to keep sealing green past a down
node, the alternative is to intersect the DB set with current membership, which preserves today's
behaviour but does not survive the mesh split — it would have to be revisited in Phase 2 anyway.
@@ -189,3 +198,25 @@ disagree on the rig. Run on `docker-dev`:
hatch, and is the check that makes step 3 acceptable to ship.
Record results in a gate doc beside this plan, per the Phase 1/Phase 2 precedent.
+
+## Task 7: `MaintenanceMode` — the escape hatch step 4 proved missing (ADDED 2026-07-22)
+
+**Classification:** high-risk — deploy path + schema
+**Why:** gate step 4 returned `422 ClusterEnabledNodeCountMismatch`. `Enabled = 0` on one node of a
+Warm/Hot pair fails `DraftValidator.ValidateClusterTopology` (enabled count must equal `NodeCount`),
+so the hatch Task 3 relies on cannot be used on any 2-node cluster — i.e. every cluster in the target
+topology. Task 3's behaviour change is only shippable with a hatch that works.
+
+**Decision:** split the meanings rather than weaken either rule.
+
+| Flag | Question | Enforced by |
+|---|---|---|
+| `Enabled` | part of the declared topology? | `DraftValidator` vs `ServerCluster.NodeCount` |
+| `MaintenanceMode` | expected to participate right now? | `ConfigPublishCoordinator`, `ClusterNodeAddressReconciler` |
+
+Rejected: counting configured rather than enabled nodes (removes the InvalidTopology guard);
+downgrading the rule to a warning (weakens a deploy gate for everyone); shipping without a hatch.
+
+**Done:** column + migration `AddClusterNodeMaintenanceMode`; both consumers filter on
+`Enabled && !MaintenanceMode`; deadline + reconciler messages name the right flag; docs; gate step 4
+re-run green.
diff --git a/docs/plans/2026-07-22-mesh-phase1-live-gate.md b/docs/plans/2026-07-22-mesh-phase1-live-gate.md
new file mode 100644
index 00000000..f9f1eb0c
--- /dev/null
+++ b/docs/plans/2026-07-22-mesh-phase1-live-gate.md
@@ -0,0 +1,134 @@
+# Per-cluster mesh Phase 1 — live gate record
+
+**Date:** 2026-07-22 · **Rig:** `docker-dev` (six-node single mesh) · **Branch:** `feat/mesh-phase1`
+**Plan:** [`2026-07-21-per-cluster-mesh-phase1.md`](2026-07-21-per-cluster-mesh-phase1.md)
+
+**Result: PASSED**, after the gate found a blocker that changed the design (step 4).
+
+Offline tests cannot cover the substitution's real risk — that the DB set and the live set disagree on
+a running fleet. This is that check.
+
+## Setup
+
+Migration applied by the rig's `migrator` service against the shared `OtOpcUa` database, then all six
+host nodes restarted on the rebuilt image. **All six pre-existing `ClusterNode` rows inherited
+`AkkaPort = 4053` from the migration's `DEFAULT 4053`** — the row-level evidence that the default does
+its job for rows predating the column:
+
+```
+central-1:4053|central-1|4053|NULL|1
+central-2:4053|central-2|4053|NULL|1
+site-a-1:4053 |site-a-1 |4053|NULL|1
+site-a-2:4053 |site-a-2 |4053|NULL|1
+site-b-1:4053 |site-b-1 |4053|NULL|1
+site-b-2:4053 |site-b-2 |4053|NULL|1
+ NodeId | Host |Akka|Grpc|En
+```
+
+## Step 1 — deploy with all six nodes up → **PASS**
+
+`POST /api/deployments` → `202 Accepted`, deployment `38f83b37`.
+
+`Deployment.Status = 2` (Sealed). Six `NodeDeploymentState` rows, every one `Status = 1` (Applied).
+**No FK 547** — the DB-derived NodeIds satisfy the `NodeDeploymentState → ClusterNode` foreign key,
+which is the standing proof that the DB set and the membership set agree on this rig.
+
+## Step 2 — *dropped, not skipped*
+
+The plan called for a cluster-scoped deploy. **There is no such thing.** `Deployment` has no
+`ClusterId`, `ConfigComposer.SnapshotAndFlattenAsync` always snapshots the whole DB, and
+`DeploymentArtifact.ResolveClusterScope` is *node-side* self-scoping of a fleet-wide artifact. Removed
+from the plan rather than faked green.
+
+## Step 3 — deploy with one node stopped → **PASS** (the new behaviour)
+
+`docker stop otopcua-dev-site-b-2-1`, then deploy `37d72301`.
+
+`Deployment.Status = 4` (TimedOut) after the 2-minute apply deadline. Five nodes `Applied`,
+`site-b-2:4053` still `Applying`. Under the membership rule this deployment would have **sealed
+green** with five nodes and never mentioned the sixth.
+
+The operator-facing log, which the plan required to be legible:
+
+```
+[12:52:02 WRN] Deployment 37d723018e374c0eb04c7b6beccc3b3e timed out after 00:02:00 (5/6 acks
+landed). No ack from: site-b-2:4053. Each is an enabled ClusterNode row — start the node, or set
+ClusterNode.Enabled = 0 if it is down for maintenance, then redeploy
+```
+
+(The remedy in that message was corrected to `MaintenanceMode = 1` — see step 4.)
+
+## Step 4 — the escape hatch → **FAILED, then fixed and re-run → PASS**
+
+### First attempt: the hatch did not exist
+
+`UPDATE ClusterNode SET Enabled = 0 WHERE NodeId = 'site-b-2:4053'`, then deploy:
+
+```
+HTTP 422
+[ClusterEnabledNodeCountMismatch] Cluster 'SITE-B' declares NodeCount=2 but has 1 Enabled nodes.
+Toggle the missing node(s) back on or change RedundancyMode/NodeCount to match.
+```
+
+`DraftValidator.ValidateClusterTopology` rejects the deployment **before it dispatches**. So
+`Enabled = 0` cannot be used on a node of a 2-node cluster — and every cluster on this rig, and every
+cluster in the program plan's target topology, is a 2-node pair.
+
+**This was a blocker, not a wrinkle.** The plan called step 4 *"the check that makes step 3
+acceptable to ship"*. Without it, a node down for maintenance blocks every deployment to its cluster,
+with no remedy short of editing `NodeCount` / `RedundancyMode` — which changes the runtime redundancy
+posture to work around a deploy gate.
+
+The two rules were each reasonable and contradictory together: the validator reads `Enabled` as *"part
+of the declared topology"*, Phase 1 additionally read it as *"expect an ack"*.
+
+### Fix (Task 7): split the meanings
+
+New `ClusterNode.MaintenanceMode bit NOT NULL DEFAULT 0`. `Enabled` keeps its topology meaning and the
+validator is untouched; the coordinator's expected-ack set and the address reconciler both filter on
+`Enabled && !MaintenanceMode`.
+
+### Re-run: PASS
+
+`docker stop` `site-b-2` + `MaintenanceMode = 1`, then deploy `82c5e678`:
+
+- `202 Accepted` — the topology gate passes, because the node is still `Enabled` (`1|1`).
+- `Deployment.Status = 2` (Sealed).
+- **Five** `NodeDeploymentState` rows, all `Applied`. `site-b-2:4053` has no row at all — excluded
+ from the expected set rather than waited for and forgiven.
+
+## Task 4 (reconciler) — live behaviour, not a planned gate step
+
+Observed unprompted during the rolling restart, which is better evidence than a staged check:
+
+```
+[12:44:49 WRN] ClusterNode address check [EnabledRowNotInCluster] site-a-1:4053: ...
+[12:44:49 WRN] ... site-a-2:4053 ... site-b-1:4053 ... site-b-2:4053
+[12:44:56 INF] ClusterNode addresses reconcile with cluster membership (6 driver members)
+```
+
+It warned per row while the site nodes were still starting, then converged to a single clean line —
+and **logged nothing further**, confirming the change-detection guard suppresses repeats on the
+5-minute sweep.
+
+Maintenance interaction, from the step 4 re-run:
+
+```
+[13:06:02 WRN] ... site-b-2:4053 ... ← node dropped, flag not yet set
+[13:06:05 INF] ... reconcile with cluster membership (5 driver members) ← flag set: silent
+```
+
+The transient warning is honest — for those three seconds the row genuinely was enabled, present in
+the expected-ack set, and absent from the cluster.
+
+## What this gate did not cover
+
+- **Phase 2's premise.** `AkkaPort` / `GrpcPort` are groundwork; nothing dials them yet, so "the value
+ is correct" is only checked by the reconciler comparing it to gossip. The first real exercise is
+ Phase 2.
+- **The reconciler under split meshes.** On the current single mesh an admin node sees every driver
+ member. After Phase 6 it will not, and every site row would report `EnabledRowNotInCluster`
+ permanently. Recorded as a known limitation on the class and in the design doc.
+- **`Enabled = 0` on a single-node (`RedundancyMode.None`) cluster.** Untested — the rig has no such
+ cluster. It would fail the same topology rule (0 enabled vs `NodeCount = 1`), so `MaintenanceMode`
+ is the hatch there too.
diff --git a/docs/v2/config-db-schema.md b/docs/v2/config-db-schema.md
index e931a08c..216a1512 100644
--- a/docs/v2/config-db-schema.md
+++ b/docs/v2/config-db-schema.md
@@ -154,6 +154,7 @@ CREATE TABLE dbo.ClusterNode (
ServiceLevelBase tinyint NOT NULL DEFAULT 200,
DriverConfigOverridesJson nvarchar(max) NULL CHECK (DriverConfigOverridesJson IS NULL OR ISJSON(DriverConfigOverridesJson) = 1),
Enabled bit NOT NULL DEFAULT 1,
+ MaintenanceMode bit NOT NULL DEFAULT 0,
LastSeenAt datetime2(3) NULL,
CreatedAt datetime2(3) NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy nvarchar(128) NOT NULL
@@ -190,10 +191,27 @@ rows against observed cluster membership and logs an Error on mismatch. It reads
than having each driver node assert its own row, because Phase 4 removes the driver nodes' ConfigDb
connection entirely.
-`Enabled` also gained a second meaning in Phase 1: it is the **expected-ack set** for a deployment.
-`ConfigPublishCoordinator` no longer derives that set from cluster membership, so an enabled row whose
-node is down now fails the deployment at the apply deadline instead of letting it seal green without
-that node. Set `Enabled = 0` for a node taken down for maintenance.
+#### `Enabled` vs `MaintenanceMode` — two flags, two questions
+
+Phase 1 made the rows the deploy path's **expected-ack set**: `ConfigPublishCoordinator` no longer
+derives it from cluster membership, so a row whose node is down now fails the deployment at the apply
+deadline instead of letting it seal green without that node.
+
+The escape hatch is **`MaintenanceMode = 1`**, not `Enabled = 0`:
+
+| Flag | Question it answers | Checked by |
+|---|---|---|
+| `Enabled` | Is this node part of the cluster's declared topology? | `DraftValidator.ValidateClusterTopology` — the enabled-node count must equal `ServerCluster.NodeCount` |
+| `MaintenanceMode` | Should we expect it to participate right now? | `ConfigPublishCoordinator` (expected-ack set) and `ClusterNodeAddressReconciler` (absence warnings) |
+
+`Enabled = 0` on one node of a Warm/Hot pair is **rejected at the deploy gate** with
+`ClusterEnabledNodeCountMismatch` — the enabled count drops to 1 while `NodeCount` stays 2. Since
+every cluster in the target topology is a 2-node pair, `Enabled` cannot serve as the maintenance
+hatch at all. The Phase 1 live gate found this; the two rules were independently reasonable and
+contradictory together, so the meanings were split rather than either rule being weakened.
+
+A node in maintenance is still enabled, so the topology gate still passes, and the runtime redundancy
+posture (`NodeCount` / `RedundancyMode`) is unchanged.
`DriverConfigOverridesJson` shape:
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs
index 52ecfddd..6214d08f 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs
@@ -63,8 +63,37 @@ public sealed class ClusterNode
public string? DriverConfigOverridesJson { get; set; }
/// Gets or sets a value indicating whether this node is enabled.
+ ///
+ /// Part of the declared topology. DraftValidator.ValidateClusterTopology requires
+ /// the enabled-node count to equal , so disabling one node
+ /// of a Warm/Hot pair is a deploy-blocking validation error by design — it would boot the
+ /// runtime into the InvalidTopology band. To take a node out of service temporarily, use
+ /// instead.
+ ///
public bool Enabled { get; set; } = true;
+ ///
+ /// Node is temporarily out of service: still part of the cluster's declared topology, but not
+ /// expected to participate. A deployment does not wait for its ack, and the address reconciler
+ /// does not warn that it is absent.
+ ///
+ ///
+ ///
+ /// Separate from because the two answer different questions.
+ /// means "this node is part of the declared topology" and is checked
+ /// against ; this flag means "do not expect it right
+ /// now". Per-cluster mesh Phase 1 made an enabled row's ack mandatory, so without this a
+ /// node down for maintenance would block every deployment to its cluster — and clearing
+ /// to escape that is itself rejected by the topology gate on any
+ /// 2-node pair, which is every cluster in the target deployment.
+ ///
+ ///
+ /// Found by the Phase 1 live gate: the two rules were independently reasonable and
+ /// contradictory together.
+ ///
+ ///
+ public bool MaintenanceMode { get; set; }
+
/// Gets or sets the timestamp when this node was last seen.
public DateTime? LastSeenAt { get; set; }
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.Designer.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.Designer.cs
new file mode 100644
index 00000000..752e3b23
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.Designer.cs
@@ -0,0 +1,1707 @@
+//
+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("20260722125506_AddClusterNodeMaintenanceMode")]
+ partial class AddClusterNodeMaintenanceMode
+ {
+ ///
+ 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", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("AkkaPort")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(4053);
+
+ 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("GrpcPort")
+ .HasColumnType("int");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("MaintenanceMode")
+ .HasColumnType("bit");
+
+ b.Property("OpcUaPort")
+ .HasColumnType("int");
+
+ b.Property("ServiceLevelBase")
+ .HasColumnType("tinyint");
+
+ b.HasKey("NodeId");
+
+ b.HasIndex("ApplicationUri")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNode_ApplicationUri");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IX_ClusterNode_ClusterId");
+
+ 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.ConfigAuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AuditId"));
+
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CorrelationId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DetailsJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EventId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Outcome")
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("Principal")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Timestamp")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("EventId")
+ .IsUnique()
+ .HasDatabaseName("UX_ConfigAuditLog_EventId")
+ .HasFilter("[EventId] IS NOT NULL");
+
+ 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.ConfigEdit", b =>
+ {
+ b.Property("EditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("EditedAtUtc")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("EditedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("EntityId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("FieldsJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceNode")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("EditId");
+
+ b.HasIndex("EditedAtUtc")
+ .HasDatabaseName("IX_ConfigEdit_EditedAt");
+
+ b.HasIndex("ExecutionId")
+ .HasDatabaseName("IX_ConfigEdit_Execution")
+ .HasFilter("[ExecutionId] IS NOT NULL");
+
+ b.HasIndex("EntityType", "EntityId")
+ .HasDatabaseName("IX_ConfigEdit_Entity");
+
+ b.ToTable("ConfigEdit", null, t =>
+ {
+ t.HasCheckConstraint("CK_ConfigEdit_FieldsJson_IsJson", "ISJSON(FieldsJson) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Deployment", b =>
+ {
+ b.Property("DeploymentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ArtifactBlob")
+ .IsRequired()
+ .HasColumnType("varbinary(max)");
+
+ b.Property("CreatedAtUtc")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("FailureReason")
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("RevisionHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("SealedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("DeploymentId");
+
+ b.HasIndex("CreatedAtUtc")
+ .HasDatabaseName("IX_Deployment_CreatedAt");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("IX_Deployment_Status");
+
+ b.ToTable("Deployment", (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("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.HasKey("DeviceRowId");
+
+ b.HasIndex("DeviceId")
+ .IsUnique()
+ .HasDatabaseName("UX_Device_LogicalId")
+ .HasFilter("[DeviceId] IS NOT NULL");
+
+ b.HasIndex("DriverInstanceId")
+ .HasDatabaseName("IX_Device_Driver");
+
+ b.HasIndex("DriverInstanceId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Device_Driver_Name");
+
+ 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("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("RawFolderId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ResilienceConfig")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.HasKey("DriverInstanceRowId");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IX_DriverInstance_Cluster");
+
+ b.HasIndex("DriverInstanceId")
+ .IsUnique()
+ .HasDatabaseName("UX_DriverInstance_LogicalId")
+ .HasFilter("[DriverInstanceId] IS NOT NULL");
+
+ b.HasIndex("RawFolderId")
+ .HasDatabaseName("IX_DriverInstance_RawFolder");
+
+ b.HasIndex("ClusterId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_DriverInstance_ClusterRoot_Name")
+ .HasFilter("[RawFolderId] IS NULL");
+
+ b.HasIndex("ClusterId", "RawFolderId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_DriverInstance_Folder_Name")
+ .HasFilter("[RawFolderId] IS NOT NULL");
+
+ b.ToTable("DriverInstance", null, t =>
+ {
+ t.HasCheckConstraint("CK_DriverInstance_DriverConfig_IsJson", "ISJSON(DriverConfig) = 1");
+
+ t.HasCheckConstraint("CK_DriverInstance_ResilienceConfig_IsJson", "ResilienceConfig IS NULL OR ISJSON(ResilienceConfig) = 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("DeviceManualUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ 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("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("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ 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("EquipmentId")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_LogicalId")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("EquipmentUuid")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Uuid");
+
+ b.HasIndex("MachineCode")
+ .HasDatabaseName("IX_Equipment_MachineCode");
+
+ b.HasIndex("SAPID")
+ .HasDatabaseName("IX_Equipment_SAPID")
+ .HasFilter("[SAPID] IS NOT NULL");
+
+ b.HasIndex("UnsLineId")
+ .HasDatabaseName("IX_Equipment_Line");
+
+ b.HasIndex("ZTag")
+ .HasDatabaseName("IX_Equipment_ZTag")
+ .HasFilter("[ZTag] IS NOT NULL");
+
+ b.HasIndex("UnsLineId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_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.LdapGroupRoleMapping", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("IsSystemWide")
+ .HasColumnType("bit");
+
+ b.Property("LdapGroup")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("LdapGroup")
+ .HasDatabaseName("IX_LdapGroupRoleMapping_Group");
+
+ b.HasIndex("LdapGroup", "ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_LdapGroupRoleMapping_Group_Cluster")
+ .HasFilter("[ClusterId] IS NOT NULL");
+
+ b.ToTable("LdapGroupRoleMapping", (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("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("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("ScopeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ScopeKind")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.HasKey("NodeAclRowId");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IX_NodeAcl_Cluster");
+
+ b.HasIndex("LdapGroup")
+ .HasDatabaseName("IX_NodeAcl_Group");
+
+ b.HasIndex("NodeAclId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_LogicalId")
+ .HasFilter("[NodeAclId] IS NOT NULL");
+
+ b.HasIndex("ScopeKind", "ScopeId")
+ .HasDatabaseName("IX_NodeAcl_Scope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.HasIndex("ClusterId", "LdapGroup", "ScopeKind", "ScopeId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_GroupScope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.ToTable("NodeAcl", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeDeploymentState", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DeploymentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AppliedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("FailureReason")
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("StartedAtUtc")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("NodeId", "DeploymentId");
+
+ b.HasIndex("DeploymentId")
+ .HasDatabaseName("IX_NodeDeploymentState_Deployment");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("IX_NodeDeploymentState_Status");
+
+ b.ToTable("NodeDeploymentState", (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("IntervalMs")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.HasKey("PollGroupRowId");
+
+ b.HasIndex("DriverInstanceId")
+ .HasDatabaseName("IX_PollGroup_Driver");
+
+ b.HasIndex("PollGroupId")
+ .IsUnique()
+ .HasDatabaseName("UX_PollGroup_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.RawFolder", b =>
+ {
+ b.Property("RawFolderRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ParentRawFolderId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RawFolderId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.HasKey("RawFolderRowId");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IX_RawFolder_Cluster");
+
+ b.HasIndex("RawFolderId")
+ .IsUnique()
+ .HasDatabaseName("UX_RawFolder_LogicalId")
+ .HasFilter("[RawFolderId] IS NOT NULL");
+
+ b.HasIndex("ClusterId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_RawFolder_ClusterRoot_Name")
+ .HasFilter("[ParentRawFolderId] IS NULL");
+
+ b.HasIndex("ClusterId", "ParentRawFolderId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_RawFolder_Parent_Name")
+ .HasFilter("[ParentRawFolderId] IS NOT NULL");
+
+ b.ToTable("RawFolder", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Script", b =>
+ {
+ b.Property("ScriptRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("ScriptId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SourceCode")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("ScriptRowId");
+
+ b.HasIndex("ScriptId")
+ .IsUnique()
+ .HasDatabaseName("UX_Script_LogicalId")
+ .HasFilter("[ScriptId] IS NOT NULL");
+
+ b.HasIndex("SourceHash")
+ .HasDatabaseName("IX_Script_SourceHash");
+
+ b.ToTable("Script", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ScriptedAlarm", b =>
+ {
+ b.Property("ScriptedAlarmRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("AlarmType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("EquipmentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HistorizeToAveva")
+ .HasColumnType("bit");
+
+ b.Property("MessageTemplate")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PredicateScriptId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Retain")
+ .HasColumnType("bit");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("ScriptedAlarmId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Severity")
+ .HasColumnType("int");
+
+ b.HasKey("ScriptedAlarmRowId");
+
+ b.HasIndex("PredicateScriptId")
+ .HasDatabaseName("IX_ScriptedAlarm_Script");
+
+ b.HasIndex("ScriptedAlarmId")
+ .IsUnique()
+ .HasDatabaseName("UX_ScriptedAlarm_LogicalId")
+ .HasFilter("[ScriptedAlarmId] IS NOT NULL");
+
+ b.HasIndex("EquipmentId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_ScriptedAlarm_EquipmentPath");
+
+ b.ToTable("ScriptedAlarm", null, t =>
+ {
+ t.HasCheckConstraint("CK_ScriptedAlarm_AlarmType", "AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
+
+ t.HasCheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ScriptedAlarmState", b =>
+ {
+ b.Property("ScriptedAlarmId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("AckedState")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("CommentsJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ConfirmedState")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("EnabledState")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("LastAckComment")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("LastAckUser")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("LastAckUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastConfirmComment")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("LastConfirmUser")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("LastConfirmUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ShelvingExpiresUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ShelvingState")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("UpdatedAtUtc")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.HasKey("ScriptedAlarmId");
+
+ b.ToTable("ScriptedAlarmState", null, t =>
+ {
+ t.HasCheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
+ });
+ });
+
+ 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")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("TagConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TagGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("TagId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("WriteIdempotent")
+ .HasColumnType("bit");
+
+ b.HasKey("TagRowId");
+
+ b.HasIndex("TagId")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_LogicalId")
+ .HasFilter("[TagId] IS NOT NULL");
+
+ b.HasIndex("DeviceId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Device_Name")
+ .HasFilter("[TagGroupId] IS NULL");
+
+ b.HasIndex("DeviceId", "TagGroupId")
+ .HasDatabaseName("IX_Tag_Device");
+
+ b.HasIndex("DeviceId", "TagGroupId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Group_Name")
+ .HasFilter("[TagGroupId] IS NOT NULL");
+
+ b.ToTable("Tag", null, t =>
+ {
+ t.HasCheckConstraint("CK_Tag_TagConfig_IsJson", "ISJSON(TagConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.TagGroup", b =>
+ {
+ b.Property("TagGroupRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ParentTagGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("TagGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("TagGroupRowId");
+
+ b.HasIndex("DeviceId")
+ .HasDatabaseName("IX_TagGroup_Device");
+
+ b.HasIndex("TagGroupId")
+ .IsUnique()
+ .HasDatabaseName("UX_TagGroup_LogicalId")
+ .HasFilter("[TagGroupId] IS NOT NULL");
+
+ b.HasIndex("DeviceId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_TagGroup_DeviceRoot_Name")
+ .HasFilter("[ParentTagGroupId] IS NULL");
+
+ b.HasIndex("DeviceId", "ParentTagGroupId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_TagGroup_Parent_Name")
+ .HasFilter("[ParentTagGroupId] IS NOT NULL");
+
+ b.ToTable("TagGroup", (string)null);
+ });
+
+ 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("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("UnsAreaId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsAreaRowId");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IX_UnsArea_Cluster");
+
+ b.HasIndex("UnsAreaId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_LogicalId")
+ .HasFilter("[UnsAreaId] IS NOT NULL");
+
+ b.HasIndex("ClusterId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_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("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("UnsAreaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsLineId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsLineRowId");
+
+ b.HasIndex("UnsAreaId")
+ .HasDatabaseName("IX_UnsLine_Area");
+
+ b.HasIndex("UnsLineId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_LogicalId")
+ .HasFilter("[UnsLineId] IS NOT NULL");
+
+ b.HasIndex("UnsAreaId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_AreaName");
+
+ b.ToTable("UnsLine", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsTagReference", b =>
+ {
+ b.Property("UnsTagReferenceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DisplayNameOverride")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("EquipmentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("TagId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsTagReferenceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsTagReferenceRowId");
+
+ b.HasIndex("EquipmentId")
+ .HasDatabaseName("IX_UnsTagReference_Equipment");
+
+ b.HasIndex("TagId")
+ .HasDatabaseName("IX_UnsTagReference_Tag");
+
+ b.HasIndex("UnsTagReferenceId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsTagReference_LogicalId")
+ .HasFilter("[UnsTagReferenceId] IS NOT NULL");
+
+ b.HasIndex("EquipmentId", "TagId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsTagReference_Equip_Tag");
+
+ b.ToTable("UnsTagReference", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.VirtualTag", b =>
+ {
+ b.Property("VirtualTagRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ChangeTriggered")
+ .HasColumnType("bit");
+
+ b.Property("DataType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("EquipmentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Historize")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("ScriptId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("TimerIntervalMs")
+ .HasColumnType("int");
+
+ b.Property("VirtualTagId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("VirtualTagRowId");
+
+ b.HasIndex("ScriptId")
+ .HasDatabaseName("IX_VirtualTag_Script");
+
+ b.HasIndex("VirtualTagId")
+ .IsUnique()
+ .HasDatabaseName("UX_VirtualTag_LogicalId")
+ .HasFilter("[VirtualTagId] IS NOT NULL");
+
+ b.HasIndex("EquipmentId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_VirtualTag_EquipmentPath");
+
+ b.ToTable("VirtualTag", null, t =>
+ {
+ t.HasCheckConstraint("CK_VirtualTag_TimerInterval_Min", "TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
+
+ t.HasCheckConstraint("CK_VirtualTag_Trigger_AtLeastOne", "ChangeTriggered = 1 OR TimerIntervalMs IS NOT 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.DriverInstance", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.LdapGroupRoleMapping", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeDeploymentState", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Deployment", "Deployment")
+ .WithMany()
+ .HasForeignKey("DeploymentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", "Node")
+ .WithMany()
+ .HasForeignKey("NodeId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Deployment");
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.RawFolder", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", null)
+ .WithMany("RawFolders")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+ });
+
+ 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.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Navigation("Credentials");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
+ {
+ b.Navigation("Nodes");
+
+ b.Navigation("RawFolders");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.cs
new file mode 100644
index 00000000..5c308d97
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260722125506_AddClusterNodeMaintenanceMode.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
+{
+ ///
+ public partial class AddClusterNodeMaintenanceMode : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "MaintenanceMode",
+ table: "ClusterNode",
+ type: "bit",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "MaintenanceMode",
+ table: "ClusterNode");
+ }
+ }
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
index 289ae370..42ebaf07 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
@@ -92,6 +92,9 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
b.Property("LastSeenAt")
.HasColumnType("datetime2(3)");
+ b.Property("MaintenanceMode")
+ .HasColumnType("bit");
+
b.Property("OpcUaPort")
.HasColumnType("int");
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs
index 07c17a3b..d955dc58 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs
@@ -27,8 +27,16 @@ namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators;
/// Behaviour change: a configured node that is switched off is now expected, so a
/// deployment dispatched while it is down fails at the apply deadline instead of sealing green
/// without it. That is deliberate — under the membership rule the operator was told the fleet was
-/// deployed when it was not. ClusterNode.Enabled = 0 is the escape hatch for a node taken
-/// down for maintenance.
+/// deployed when it was not. ClusterNode.MaintenanceMode = 1 is the escape hatch for a node
+/// taken down for maintenance.
+///
+///
+///