AdminUI driver-instance pages serialized enum config fields (S7 CpuType,
Modbus DataType/Region, AbCip PlcFamily, ...) as JSON *numbers* because each
page's _jsonOpts lacked a JsonStringEnumConverter. The driver factories,
however, deserialize into string-typed DTOs (+ lenient ParseEnum) and throw
when binding a JSON number to a string? — so an AdminUI-authored config
containing any enum field produced a blob the driver could not parse,
faulting the driver on deploy. Proven end-to-end for S7 and Modbus; latent
for AbCip/AbLegacy/TwinCAT/FOCAS/Galaxy/Historian. Only OpcUaClient was safe
(its factory + probe already carried the converter).
Add JsonStringEnumConverter to all 9 driver-instance pages' _jsonOpts and the
8 missing driver probes' _opts (factories unchanged — already string-via-
ParseEnum; strictly more permissive, also lets pages load hand-seeded
string-enum configs back into the form).
Also fix DriverProbeHandshakeE2eTests.AbCip_Green_AgainstSim to probe a real
sim tag (TestDINT) — the no-tags @raw_cpu_type fallback is rejected by the
ab_server sim with ErrorBadParam (a real ControlLogix returns ErrorNotFound,
which the probe treats as reachable; hardware-gated follow-up).
Tests: reflection guard over all driver pages' _jsonOpts (AdminUI.Tests);
factory round-trip + numeric-form-throws guards for S7 and Modbus.
Found by running the never-before-run FB-9/FB-10 live verifies.
Subscribe ConnectionStateChanged before reading IsConnected (subscribe-then-read
idiom, matches DriverStatusPanel) so no transition is missed. Add
OnConnectionStateChanged handler that marshals to the circuit sync context via
InvokeAsync. Dispose unsubscribes both events.
Reconnect-overlay: App.razor loads _framework/blazor.web.js and contains no
custom #components-reconnect-modal element; .NET 10 Blazor's default reconnect
overlay is active automatically — no custom markup needed.
No unit tests; live-verify follows.
Dispose the CancellationTokenSource in AcknowledgeAsync and ShelveAsync
(the TimeSpan overload holds an internal timer — leaked without using).
Add StateHasChanged() to ShowOpResult so the result chip renders even if
a future caller omits the finally-block re-render.
T21: add an AdminUI path for acknowledging/shelving alarms that routes
through the admin-pinned AdminOperationsActor cluster singleton, which
republishes onto the same 'alarm-commands' DPS topic the OPC UA method
path (T18) and the engine subscriber (T19) use. The broadcast + the
ScriptedAlarmHostActor ownership filter handle cross-node routing, so
the singleton needs no knowledge of which node owns the alarm.
- Commons: AcknowledgeAlarmCommand/ShelveAlarmCommand (+ result records)
and a shared AlarmCommandsTopic const; ScriptedAlarmHostActor now
re-exports that const (mirrors the DriverControlTopic pattern).
- AdminOperationsActor: two handlers map the control-plane messages to
AlarmCommand (Acknowledge / OneShotShelve / TimedShelve / Unshelve,
threading User/Comment/UnshelveAtUtc) and publish via the DPS mediator.
- IAdminOperationsClient + AdminOperationsClient: typed Acknowledge/Shelve
ask wrappers mirroring StartDeploymentAsync.
- Alerts.razor: per-row DriverOperator-gated Ack/Shelve/Unshelve controls;
operator name from AuthenticationState. Timed-shelve datetime UI deferred.
- 5 TestKit tests (mediator-probe subscribed to alarm-commands) verifying
each kind's mapping + reply; 56/56 ControlPlane tests green.
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.
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.
Audit (task #134) found the same Razor literal-binding bug class as the UNS
Filter fix (14b4692): a string-typed component parameter assigned without a
leading @ is a LITERAL, not an expression. Confirmed against the generated
.g.cs (literal "_error" vs TypeCheck<String>(_error)).
- DriverFormShell Error="_error" -> "@_error" on all 9 driver edit pages:
Error received the constant "_error", so the error banner rendered
permanently and the real failure message was never shown.
- DriverBrowseTree SelectedNodeId="_tagName"/"_nodeId" -> "@..." in the
Galaxy and OpcUaClient address pickers: the tree's selected-node highlight
compared against a literal that never matched a real node.
Build clean; generated code now binds all 11 as TypeCheck<String>(field);
AdminUI.Tests 216/216 green.
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).
Deletes the 10 Razor pages superseded by the global /uns tree (Tasks 12–16):
ClusterUns, UnsAreaEdit, UnsLineEdit, ClusterEquipment, EquipmentEdit,
ImportEquipment, ClusterTags, TagEdit, VirtualTags, VirtualTagEdit.
No dangling references found; build is clean.