AdminUI cluster-node editor was dead: HTTP 500 on load (InputNumber<byte>) + Save a silent no-op (NodeId regex forbids ':') #505

Closed
opened 2026-07-26 10:43:27 -04:00 by dohertj2 · 0 comments
Owner

Found while live-verifying the #499 guard, which lives on this page. Both defects are pre-existing on master and unrelated to that change. Fixed in 4cc0215b on fix/issues-503-501-493-499-488; filing this so the finding is recorded independently of the issue that happened to surface it.

/clusters/{clusterId}/nodes/{nodeId} and /clusters/{clusterId}/nodes/new were completely non-functional.

1. HTTP 500 on load

FormModel.ServiceLevelBase was declared byte to match ClusterNode.ServiceLevelBase, and bound to <InputNumber>. InputNumber<T>'s static constructor throws for System.Byte:

System.TypeInitializationException: The type initializer for
'Microsoft.AspNetCore.Components.Forms.InputNumber`1' threw an exception.
 ---> System.InvalidOperationException: The type 'System.Byte' is not a supported numeric type.
   at Microsoft.AspNetCore.Components.Forms.InputNumber`1.GetStepAttributeValue()
   at Microsoft.AspNetCore.Components.Forms.InputNumber`1.BuildRenderTree(RenderTreeBuilder builder)

Unhandled inside BuildRenderTree, so it takes the whole page, not one field — the same failure mode as #504. Blazor's supported InputNumber types are int/long/short/float/double/decimal (and nullables); byte is not among them.

Fix: int on the form model with the same [Range(0, 255)], cast to byte on the way to the entity. Stored type unchanged.

2. Save did nothing at all, silently

NodeId was validated against ^[A-Za-z0-9_-]+$which forbids the colon. Every NodeId in this system is host:port: ClusterRoleInfo and ConfigPublishCoordinator derive it from member.Address.Host:Port, the docker-dev seed writes central-1:4053, and NodeDeploymentState.NodeId is FK-bound to it.

So on every existing node, validation failed, OnValidSubmit never ran, and the Save button was inert.

Fix: ^[A-Za-z0-9_.:-]+$ (hostnames may also be dotted — line3-opc-a.plant.local:4053).

3. Why nobody could tell — the missing <ValidationSummary/>

The form had a <DataAnnotationsValidator /> but nothing rendering its output, and no per-field <ValidationMessage>. A validation failure produced zero feedback: the button simply did nothing. That is how (2) stayed invisible, and it is the reason this is worth a separate issue rather than a footnote — the missing summary is a defect generator, not just a symptom. Added.

Verification

Live on docker-dev, both central nodes rebuilt (:9200 round-robins the pair):

  • Page: 500 → 200 on /clusters/MAIN/nodes/central-1:4053 and /clusters/MAIN/nodes/new, and it renders fully.
  • Save: a real edit to DriverConfigOverridesJson now persists (confirmed in SQL). Rig restored afterwards.

Worth following up

This page has no automated coverage that would have caught either defect — consistent with the standing note that the AdminUI has no bUnit. A cheap partial guard would be a reflection test asserting no InputNumber in the codebase is bound to an unsupported numeric type; the silent-validation class is better handled by a convention that every EditForm carries a ValidationSummary. Neither is done here.

Found while live-verifying the #499 guard, which lives on this page. **Both defects are pre-existing on master** and unrelated to that change. Fixed in `4cc0215b` on `fix/issues-503-501-493-499-488`; filing this so the finding is recorded independently of the issue that happened to surface it. `/clusters/{clusterId}/nodes/{nodeId}` and `/clusters/{clusterId}/nodes/new` were **completely non-functional**. ## 1. HTTP 500 on load `FormModel.ServiceLevelBase` was declared `byte` to match `ClusterNode.ServiceLevelBase`, and bound to `<InputNumber>`. `InputNumber<T>`'s **static constructor** throws for `System.Byte`: ``` System.TypeInitializationException: The type initializer for 'Microsoft.AspNetCore.Components.Forms.InputNumber`1' threw an exception. ---> System.InvalidOperationException: The type 'System.Byte' is not a supported numeric type. at Microsoft.AspNetCore.Components.Forms.InputNumber`1.GetStepAttributeValue() at Microsoft.AspNetCore.Components.Forms.InputNumber`1.BuildRenderTree(RenderTreeBuilder builder) ``` Unhandled inside `BuildRenderTree`, so it takes the **whole page**, not one field — the same failure mode as #504. Blazor's supported `InputNumber` types are `int`/`long`/`short`/`float`/`double`/`decimal` (and nullables); `byte` is not among them. **Fix:** `int` on the form model with the same `[Range(0, 255)]`, cast to `byte` on the way to the entity. Stored type unchanged. ## 2. Save did nothing at all, silently `NodeId` was validated against `^[A-Za-z0-9_-]+$` — **which forbids the colon**. Every NodeId in this system is `host:port`: `ClusterRoleInfo` and `ConfigPublishCoordinator` derive it from `member.Address.Host:Port`, the docker-dev seed writes `central-1:4053`, and `NodeDeploymentState.NodeId` is FK-bound to it. So on **every existing node**, validation failed, `OnValidSubmit` never ran, and the Save button was inert. **Fix:** `^[A-Za-z0-9_.:-]+$` (hostnames may also be dotted — `line3-opc-a.plant.local:4053`). ## 3. Why nobody could tell — the missing `<ValidationSummary/>` The form had a `<DataAnnotationsValidator />` but **nothing rendering its output**, and no per-field `<ValidationMessage>`. A validation failure produced zero feedback: the button simply did nothing. That is how (2) stayed invisible, and it is the reason this is worth a separate issue rather than a footnote — the missing summary is a defect *generator*, not just a symptom. Added. ## Verification Live on docker-dev, both central nodes rebuilt (`:9200` round-robins the pair): - Page: **500 → 200** on `/clusters/MAIN/nodes/central-1:4053` and `/clusters/MAIN/nodes/new`, and it renders fully. - Save: a real edit to `DriverConfigOverridesJson` now persists (confirmed in SQL). Rig restored afterwards. ## Worth following up This page has no automated coverage that would have caught either defect — consistent with the standing note that the AdminUI has no bUnit. A cheap partial guard would be a reflection test asserting no `InputNumber` in the codebase is bound to an unsupported numeric type; the silent-validation class is better handled by a convention that every `EditForm` carries a `ValidationSummary`. Neither is done here.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#505