diff --git a/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json b/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json index e77928e6..df9b97d7 100644 --- a/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json +++ b/archreview/plans/R2-07-surgical-pure-adds-plan.md.tasks.json @@ -79,7 +79,7 @@ { "id": "T9", "subject": "Phase 2: OtOpcUaNodeManager.RemoveAlarmConditionNode (+ native-flag cleanup) (high-risk)", - "status": "pending", + "status": "completed", "blockedBy": [ "T7" ] diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index a09a7e58..cbcb0de2 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -1823,7 +1823,28 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } /// - public bool RemoveAlarmConditionNode(string alarmNodeId) => false; + public bool RemoveAlarmConditionNode(string alarmNodeId) + { + ArgumentException.ThrowIfNullOrEmpty(alarmNodeId); + EnsureAddressSpaceCreated(); + + GeneralModelChangeEventState e; + lock (Lock) + { + // Unknown id ⇒ map drift ⇒ false (caller rebuilds). Mirrors the condition teardown inside + // RebuildAddressSpace, scoped to this one condition. The equipment folder's event-notifier + // promotion is intentionally NOT demoted here — other conditions may still hang under it, and a + // lingering notifier folder is harmless (RemoveEquipmentSubtree demotes it when the whole + // equipment goes). + if (!_alarmConditions.TryRemove(alarmNodeId, out var condition)) return false; + condition.Parent?.RemoveChild(condition); + PredefinedNodes?.Remove(condition.NodeId); + _nativeAlarmNodeIds.Remove(alarmNodeId); + e = BuildNodesRemovedModelChange(alarmNodeId); + } + ReportModelChangeOutsideLock(e, alarmNodeId); + return true; + } /// public bool RemoveEquipmentSubtree(string equipmentNodeId) => false;