namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// Physical OPC UA server node within a . public sealed class ClusterNode { /// Stable per-machine logical ID, e.g. "LINE3-OPCUA-A". public required string NodeId { get; set; } /// The unique identifier of the cluster this node belongs to. public required string ClusterId { get; set; } /// Machine hostname / IP. public required string Host { get; set; } /// The OPC UA server port (default 4840). public int OpcUaPort { get; set; } = 4840; /// The dashboard HTTP port (default 8081). public int DashboardPort { get; set; } = 8081; /// /// Akka remoting port (default 4053). This is central's dial target, not the node's own /// binding configuration — the node binds from Cluster:Port in its own appsettings, /// and this column duplicates that value for a reader that cannot see the node's config. /// Per-cluster mesh Phase 2 builds its ClusterClient contact points from /// + this port, at which point central and the site node no longer share a /// gossip ring and central has no other way to learn it. /// /// /// The duplication is real and unenforced by the schema. ClusterNodeAddressReconciler /// on the admin node compares this row against observed cluster membership and logs an Error /// on mismatch — a node that binds 4054 while its row says 4053 is unreachable from central in /// Phase 2, and the symptom there is a silent absence of acks rather than an error. /// public int AkkaPort { get; set; } = 4053; /// /// gRPC port central dials for the Phase 5 telemetry stream, or when the /// node exposes none. Also central's dial target rather than the node's binding config. /// /// /// Nullable by intent: nothing listens on this port yet, and a non-null default would assert a /// port that does not exist. Phase 5 populates it; until then is the /// honest value. /// public int? GrpcPort { get; set; } /// /// OPC UA ApplicationUri — MUST be unique per node per OPC UA spec. Clients pin trust here. /// Fleet-wide unique index enforces no two nodes share a value. /// Stored explicitly, NOT derived from at runtime — silent rewrite on /// hostname change would break all client trust. /// public required string ApplicationUri { get; set; } /// Primary = 200, Secondary = 150 by default. public byte ServiceLevelBase { get; set; } = 200; /// /// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig /// at apply time. Minimal by intent. Nullable when no overrides exist. /// 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; } /// Gets or sets the timestamp when this node was created. public DateTime CreatedAt { get; set; } = DateTime.UtcNow; /// Gets or sets the username of who created this node. public required string CreatedBy { get; set; } // Navigation /// Gets or sets the cluster this node belongs to. public ServerCluster? Cluster { get; set; } /// Gets or sets the credentials associated with this node. public ICollection Credentials { get; set; } = []; }