Default HistorizeToAveva/Retain/Enabled to the entity defaults (true) when a
field is absent/null/non-boolean so a partial blob decodes identically to the
composer's view of a default-constructed ScriptedAlarm (byte-parity), and only
call GetBoolean for a genuine true/false token. Add direct ExtractAlarmDependencyRefs
unit tests (overlap dedup + reserved {{equip}} exclusion).
Concrete ITagUpstreamSource the scripted-alarm host actor pushes
DependencyValueChanged values into and ScriptedAlarmEngine reads/subscribes
from. Thread-safe: ConcurrentDictionary value cache + per-path ImmutableList
observer lists with atomic add/remove and capture-then-invoke fan-out.
ReadTag of an unknown path returns a Bad-quality (0x80000000) snapshot stamped
via the injected clock. Adds the Core.ScriptedAlarms project reference Runtime
needs to see the interface.
ScriptId is now system-generated on create (mirrors EquipmentId's EQ-{12 hex}
convention, never operator-supplied) and shown read-only when editing. Language
is always CSharp, so the single-option dropdown is removed entirely and set on
save.
Monaco's word definition splits on '.', so accepting a full tag path while a
partial path was typed (e.g. "X.Protected") duplicated the prefix
(-> "X.X.ProtectedValue"). Tag-path items now replace the whole literal
content from the opening quote to the caret.
The suggest/hover popups were offset far from the caret because the theme's
.rise entrance animation leaves a CSS transform on an ancestor, which becomes
the containing block for the position:fixed overflow widgets. Render them in a
body-level overflowWidgetsDomNode so they stay viewport-correct at the caret.
The per-driver editor models expose Validate() (required-field checks) but the
TagModal never called them, so a blank required field (e.g. S7 address, AbCip
tag path) saved silently and only failed at deploy/connect. Add a
TagConfigValidator registry (DriverType -> model.FromJson(json).Validate(),
parallel to TagConfigEditorMap) and call it in SaveAsync before the service
call — a non-null result sets the modal error and blocks save. Unmapped drivers
(no typed editor) and Modbus (no required field) return null. Editors untouched.
AdminUI.Tests 307/307 (12 new validator tests); build clean.
The /uns filter was per-level: it matched only a node's direct children by
DisplayName and only under already-expanded nodes, so typing "blender" at the
top matched nothing — the structural ancestors don't contain the text and
weren't expanded.
Rework UnsTree to the standard tree-filter behaviour:
- A node is shown if it self-matches, sits under a matched ancestor, or has a
matching descendant (VisibleUnder).
- The path to a match auto-expands (chevron + child block follow a filter-
derived `childrenShown`, not node.Expanded), and the whole subtree under a
matched node is shown.
- Lazy tag children are only considered once their equipment is loaded, so the
filter never triggers lazy loads; the bounded structural tree keeps the
recursive walk cheap.
Clearing the filter restores the user's manual expand state (node.Expanded is
untouched). Build clean; AdminUI.Tests 216/216.
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.