The equipment page's Remove reference / Delete virtual tag / Delete alarm row
buttons and the ScriptEdit Delete ran on the first click. They now go through
RawConfirmDialog, the same confirm /raw uses. The ScriptEdit change is limited
to the delete button and its dialog.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed.
m1: DisposeBridge cancels its CTS when the 5 s wait times out.
m2: a CSV import that commits after the modal closed still raises OnSaved.
m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md.
m4: browsePageDelayMs written only when loaded or changed; negative flagged.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
An area or line could be named "Packaging" or "Line 1", save cleanly, and then
fail the NEXT deployment with UnsSegmentInvalid. Because a deployment snapshots
the whole database, one badly-named area blocked EVERY cluster's deploy - and
whoever hit that was usually not whoever typed the name. Only the equipment form
carried the rule; the deploy gate held its own private copy.
The rule now lives once, on Configuration.Validation.UnsSegment, and both ends
call it. DraftValidator no longer restates it.
Enforced in UnsTreeService, not just on the forms:
- Create/Update Area, Line and Equipment reject an invalid name before touching
the row.
- The CSV import reports 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 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, but it now references the shared
pattern rather than restating it - a second copy is how these drifted apart.
One behaviour change beyond the fix: UnsSegment.AttributePattern admits
"_default", which the equipment form's hand-written literal rejected. The form
was stricter than the gate it was mirroring, so a _default row was uneditable
through the AdminUI.
Tests: 41, falsified with three production mutations - dropping the CreateArea
guard reds exactly the CreateArea rejections, turning the import error back into
a skip reds only the import test, and narrowing AttributePattern reds only the
attribute/IsValid agreement. Every rejection is paired with a name that must
still be accepted, or a guard that refused everything would pass.
Live-verified on docker-dev: the area modal now refuses "Packaging" in place
with the rule spelled out, "packaging-b" saves, and a CSV import of two rows
reports Inserted 1 / Skipped 0 / Errors 1 naming the offending row - the path no
form attribute reaches. Rig cleaned up and a further deployment sealed.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
Replace the Batch-1-stubbed authored-tag flow with a UnsTagReference list on the
equipment page's Tags tab (v3 reference-only equipment): columns are effective name,
computed raw path, inherited datatype/access (read-only), display-name override
(editable), and remove. The retired equipment-authored-tag editor (TagModal.razor)
is deleted; the VirtualTag and ScriptedAlarm flows are untouched.
- "+ Add reference" opens a new AddReferenceModal that reuses RawTree in a new opt-in
PickerMode (Tag-leaf checkboxes + Device/TagGroup "select all tags below"), scoped
structurally to the equipment's cluster via LoadReferencePickerRootAsync (a single
cluster root — cross-cluster tags are unreachable, not merely hidden). Default /raw
usage is byte-unchanged (PickerMode defaults false).
- UnsTreeService (+ IUnsTreeService) gain the reference mutations:
LoadReferencesForEquipmentAsync (computes each RawPath via the shared RawPathResolver),
AddReferencesAsync (cluster-checked, all-or-nothing), RemoveReferenceAsync,
SetReferenceOverrideAsync, plus the picker helpers LoadReferencePickerRootAsync /
LoadDescendantTagIdsAsync.
- Consume IEffectiveNameGuard (constructor-injected, no-op fallback when unregistered):
AddReferences, SetReferenceOverride, and the VirtualTag + ScriptedAlarm create/update
mutations now call CheckAsync before persisting and surface a collision as the failure.
WP2 supplies the implementation + DI registration.
- Drop the retired equipment↔driver binding: remove DriverInstanceId from EquipmentInput,
the ImportEquipmentModal CSV column, the EquipmentPage Details driver select, and the
dead decision-#122 driver-cluster guard.
Tests: new UnsTreeServiceReferenceTests (add/remove/override, cross-cluster + duplicate
rejection, guard-consumed rejection via a fake guard, RawPath projection, picker helpers);
Equipment/Import test suites rewritten to drop the retired driver-guard cases. AdminUI
suite green (639 passed, 3 skipped); full solution builds 0 errors.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
Low-severity review nits, no behaviour change to the happy path:
- CloseModals() now also resets the leftover _*ModalIsNew / parent-id fields
(area ClusterId, line AreaId, equipment LineId, tag/vtag) for symmetry —
harmless today (always set before a modal opens) but consistent.
- HandleAddChild / HandleAddVirtualTag / HandleEdit gain a _modalBusy guard
(try/finally) so a rapid double-action can't race two service loads into the
same modal state. The switch bodies are re-indented under the try block.
- VirtualTagModal DataType is now an InputSelect over the standard OPC UA type
list (the same set TagModal uses) instead of free-text InputText.
- RefreshEquipmentChildrenAsync documents that callers own StateHasChanged()
and the full-reload fallback is spelled out as a block with a comment.
Build clean; AdminUI.Tests 216/216.
GlobalUns passed string component params without an @ prefix
(Filter="_filter", ClusterId="_areaModalClusterId", etc.). Razor treats a
string-typed component-parameter value without @ as a LITERAL, so UnsTree.Filter
became the literal "_filter" and the modals received literal field-name strings
as their parent ids. The non-empty literal filter matched no node, so the tree
never rendered children beyond the enterprise roots; the modals would have created
children under a bogus cluster/area/line/equipment id.
Add @ to the six string-param bindings. Verified live in docker-dev: the full
Enterprise->Cluster->Area->Line->Equipment tree renders and an area created via the
modal persists with the correct ClusterId (MAIN). No unit test added — this is a
Razor binding issue not reachable without bUnit (not used in this project).