diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NodeEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NodeEdit.razor index 8f663bb5..a246c14b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NodeEdit.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NodeEdit.razor @@ -36,6 +36,10 @@ else { + @* Without this, a validation failure suppresses OnValidSubmit with NO feedback whatsoever — + the Save button simply does nothing and the operator has no way to tell why. That is how the + NodeId-regex defect above stayed invisible. *@ +
Identity
@@ -223,7 +227,7 @@ else OpcUaPort = _form.OpcUaPort, DashboardPort = _form.DashboardPort, ApplicationUri = _form.ApplicationUri, - ServiceLevelBase = _form.ServiceLevelBase, + ServiceLevelBase = (byte)_form.ServiceLevelBase, Enabled = _form.Enabled, DriverConfigOverridesJson = string.IsNullOrWhiteSpace(_form.DriverConfigOverridesJson) ? null : _form.DriverConfigOverridesJson, CreatedAt = DateTime.UtcNow, @@ -239,7 +243,7 @@ else entity.OpcUaPort = _form.OpcUaPort; entity.DashboardPort = _form.DashboardPort; entity.ApplicationUri = _form.ApplicationUri; - entity.ServiceLevelBase = _form.ServiceLevelBase; + entity.ServiceLevelBase = (byte)_form.ServiceLevelBase; entity.Enabled = _form.Enabled; entity.DriverConfigOverridesJson = string.IsNullOrWhiteSpace(_form.DriverConfigOverridesJson) ? null : _form.DriverConfigOverridesJson; } @@ -283,14 +287,26 @@ else private sealed class FormModel { - [Required, RegularExpression("^[A-Za-z0-9_-]+$")] + // The ':' and '.' are REQUIRED, not permissive. Every NodeId in this system is "host:port" — + // ClusterRoleInfo and ConfigPublishCoordinator derive it as member.Address.Host:Port, the seed + // writes "central-1:4053", and NodeDeploymentState.NodeId is FK-bound to it. The previous + // pattern forbade the colon, so EVERY existing node failed validation and OnValidSubmit never + // ran: the Save button did nothing at all, silently. Hostnames may also be dotted + // (line3-opc-a.plant.local:4053). + [Required, RegularExpression("^[A-Za-z0-9_.:-]+$")] public string NodeId { get; set; } = ""; [Required] public string Host { get; set; } = ""; [Range(1, 65535)] public int OpcUaPort { get; set; } = 4840; [Range(1, 65535)] public int DashboardPort { get; set; } = 8081; [Required, RegularExpression("^urn:[A-Za-z0-9_:./-]+$")] public string ApplicationUri { get; set; } = ""; - [Range(0, 255)] public byte ServiceLevelBase { get; set; } = 200; + // int, NOT byte. Blazor's InputNumber rejects System.Byte outright — its static + // constructor throws "The type 'System.Byte' is not a supported numeric type", which escapes + // BuildRenderTree unhandled and takes the WHOLE page to an HTTP 500. Binding this field to + // the entity's own byte type meant /clusters/{id}/nodes/{nodeId} and .../nodes/new never + // rendered at all. The Range attribute still holds it to a byte's domain, and SubmitAsync + // casts on the way to the entity, so the stored type is unchanged. + [Range(0, 255)] public int ServiceLevelBase { get; set; } = 200; public bool Enabled { get; set; } = true; public string? DriverConfigOverridesJson { get; set; } }