From 4cc0215bb428bf6ff05e5e1d53a38e1a557bd991 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 26 Jul 2026 10:42:04 -0400 Subject: [PATCH] =?UTF-8?q?fix(adminui):=20the=20cluster-node=20editor=20w?= =?UTF-8?q?as=20dead=20=E2=80=94=20500=20on=20load,=20silent=20no-op=20on?= =?UTF-8?q?=20save?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both defects are PRE-EXISTING (present on master, unrelated to #499) and were found live-verifying the #499 guard, which lives on this page and was unreachable without them fixed. 1. **HTTP 500 on load.** `FormModel.ServiceLevelBase` was `byte` to match the entity, and `InputNumber`'s static constructor throws outright for `System.Byte` — "The type 'System.Byte' is not a supported numeric type". That escapes `BuildRenderTree` unhandled, which in Blazor takes the WHOLE page down, not one field. `/clusters/{id}/nodes/{nodeId}` and `.../nodes/new` have never rendered. Now `int` with the same `[Range(0, 255)]` and a cast on the way to the entity, so the stored type is unchanged. (Same failure mode as #504: an unhandled throw inside the render tree is a page-kill.) 2. **Save did nothing, silently.** `NodeId` was validated against `^[A-Za-z0-9_-]+$`, which forbids the colon — but 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). So every existing node failed validation, `OnValidSubmit` never ran, and the button was inert. Pattern now allows `:` and `.` (hostnames may be dotted). 3. **Added the missing ``.** The form had a `DataAnnotationsValidator` but nothing rendering its output, so a validation failure produced no feedback at all — which is precisely how (2) stayed invisible. This is the change that stops the next such defect being silent. Live-verified on docker-dev (both central nodes rebuilt, since :9200 round-robins the pair): page 500 → 200 on both routes, and a real edit now persists. --- .../Components/Pages/Clusters/NodeEdit.razor | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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; } }