AdminUI: /uns area + line names bypass the UNS segment rule, blocking fleet-wide deploys #535
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The
/unsarea and line creation modals do not validate the UNS segment format. Only equipment does (EquipmentPage.razor:570,^[a-z0-9-]{1,32}$with a hint under the field).Reproduced on docker-dev while authoring a UNS under
SITE-A: an area namedPackagingand a line namedLine 1both saved without complaint, and the next deployment failed withDraftValidator.ValidateUnsSegmentschecks all three levels (DraftValidator.cs:604-620); the AdminUI checks one.Why this is worse than it looks: a deployment snapshots the whole database, so one badly-named area in one cluster blocks every cluster's deploy until someone finds it. The operator who typed the name may not be the one who discovers the fleet cannot deploy.
This is the same authoring-vs-gate gap #520 stage 7 just closed for
NodeAclrows — the fix there was to make the page apply the deploy gate's own rule. Here it is smaller: the regex and message already exist on the equipment form, so the area and line modals need the same[RegularExpression]plus the same hint text. Renaming an existing area/line is a separate path and should get it too.Found incidentally; not fixed as part of stage 7 (out of scope). Recorded in
docs/plans/2026-07-31-apikey-acl-enforcement-design.mdunder the stage-7 live gate.Fixed.
master1a1dbc3f, pushed.The rule now has one home
Configuration.Validation.UnsSegment—IsValid,AttributePattern,Message,Describe.DraftValidatorno longer restates it, and the forms reference it instead of holding a literal. A second copy is how these drifted apart in the first place.Enforced in the service, not just on the forms
CreateAreaAsync/UpdateAreaAsync,CreateLineAsync/UpdateLineAsync,CreateEquipmentAsync/UpdateEquipmentAsyncreject an invalid name before touching the row.ImportEquipmentAsyncreports a bad name as an error, not a skip. A skip means "you already have this row", which would tell the operator the equipment exists when it does not. Good rows in the same batch still insert.A form attribute could never have covered the CSV import, and with no bUnit in this repo a rule only a form enforces is also a rule no test can reach. The forms keep an attribute for the immediate message, plus a hint under the field so the mistake is prevented rather than reported.
One behaviour change beyond the fix
AttributePatternadmits_default, which the equipment form's hand-written literal rejected. The form was stricter than the gate it was mirroring, so a_defaultrow was uneditable through the AdminUI. Pinned by a test.Verification
41 tests, falsified with three production mutations, each reddening exactly its own tests:
CreateAreaguard (the original bug) → the 7CreateArearejectionsAttributePatternto the bare pattern → only the attribute/IsValidagreement testEvery rejection is paired with a name that must still be accepted — a guard that refused everything would satisfy the rejection half alone.
Live-verified on docker-dev:
Packagingin place, with the rule spelled out and the modal held open.packaging-bsaves — the positive control.Inserted: 1 · Skipped: 0 · Errors: 1,Row 'packer_003': equipment name 'Packer 3' does not match [a-z0-9-]{1,32} or '_default'.That is the leg that matters: it is the path no form attribute reaches.Rig cleaned up; a further deployment sealed. Documented in
docs/Uns.md§"Naming: area, line and equipment names are UNS path segments", including the id-vs-name distinction (UnsAreaIdtakes^[A-Za-z0-9_-]+$and never reaches a NodeId, soSITEA-area1is a fine id for an area namedpackaging).