7.9 KiB
Still-Pending Phase 7 — Client.UI alarm Ack/Shelve/Confirm buttons — design
Status: approved 2026-06-16 (UI surface = "extend context menu", Confirm gating = "simple", both via AskUserQuestion). Parent roadmap:
docs/plans/2026-06-15-stillpending-backlog-design.md(Phase 7). Branchfeat/stillpending-phase-7-client-alarm-buttonsoff masterad3ec9d9. Phases 0–6 already shipped.
Goal
Give the Avalonia desktop client (Client.UI) operator-invokable Acknowledge / Shelve / Confirm
on the AlarmsView, via the existing right-click context menu, backed by the already-implemented
IOpcUaClientService methods (Client.CLI already has all three). Closes the stillpending.md §4
Client.UI gaps: "AlarmsView has no per-row Acknowledge button" + "Shelve/Confirm have no UI surface".
Grounding (verified against master ad3ec9d9)
- AlarmsView (
src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/AlarmsView.axaml{,.cs}) is aDataGrid; its code-behind builds a runtime context menu inOnLoadedwith ONE item, "Acknowledge…", shown whenCanAcknowledge, openingAckAlarmWindow→AlarmsViewModel.AcknowledgeAlarmAsync. - MVVM = CommunityToolkit.Mvvm (
[RelayCommand],ObservableObject). IOpcUaClientService(Client.Shared/IOpcUaClientService.cs) ALREADY has:AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment, ct),ConfirmAlarmAsync(string conditionNodeId, byte[] eventId, string comment, ct),ShelveAlarmAsync(string conditionNodeId, ShelveKind kind, double shelvingTimeSeconds = 0, ct).ShelveKind= OneShot / Timed / Unshelve. Shelve takes NO comment; duration is in seconds (the service converts to ms). All three returnTask<StatusCode>.- Client.CLI
ack/confirm/shelvecommands already complete — the reference param shapes. AlarmEventViewModelexposesConditionNodeId,EventId,ActiveState,AckedState,Retain,Severity,Message,Time, andCanAcknowledge(ActiveState && !AckedState && EventId != null && ConditionNodeId != null). NOCanShelve/CanConfirm, NOShelvedState/ConfirmedState.AlarmsViewModel.AcknowledgeAlarmAsync(alarm, comment)exists and returns(bool Success, string Message); NO Shelve/Confirm equivalents.- Tests:
tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/(AlarmsViewModelTests+ a UI-sideFakeOpcUaClientServicetest double). CLI side hasShelveCommandTests/ConfirmCommandTests.
Locked decisions
- UI surface = extend the existing right-click context menu — add "Shelve…" + "Confirm…" beside "Acknowledge…", mirroring the existing wiring. (Not inline-button columns nor a toolbar.)
- Confirm gating = simple —
CanConfirm = AckedState && EventId != null && ConditionNodeId != null. NoConfirmedStateevent-parsing tracking (the server rejects a redundant Confirm; the result banner surfaces the Bad). Avoids touching the Client.Shared event-mapping path.
Components
AlarmsViewModel(…/ViewModels/AlarmsViewModel.cs) — add two methods mirroringAcknowledgeAlarmAsync:Task<(bool Success, string Message)> ShelveAlarmAsync(AlarmEventViewModel alarm, ShelveKind kind, double durationSeconds)— guardIsConnected && alarm.ConditionNodeId != null(EventId NOT needed for shelve); guard Timed ⇒durationSeconds > 0(friendly fail BEFORE the call); call_service.ShelveAlarmAsync(alarm.ConditionNodeId, kind, durationSeconds); mapStatusCode→(Success, Message).Task<(bool Success, string Message)> ConfirmAlarmAsync(AlarmEventViewModel alarm, string comment)— guardIsConnected && ConditionNodeId != null && EventId != null; call_service.ConfirmAlarmAsync(nodeId, eventId, comment); map result. (Byte-for-byte mirror ofAcknowledgeAlarmAsync.)
AlarmEventViewModel(…/ViewModels/AlarmEventViewModel.cs) — add computed:bool CanShelve => ConditionNodeId != null;bool CanConfirm => AckedState && EventId != null && ConditionNodeId != null;
ShelveAlarmWindow(NEW —…/Views/ShelveAlarmWindow.axaml{,.cs}) — mirrorAckAlarmWindow's shape (source/condition header + result banner). Inputs: a ShelveKind selector (ComboBox or RadioButtons: OneShot / Timed / Unshelve, default OneShot) + a Duration (seconds) input enabled ONLY when Timed. "Shelve" button →_alarmsVm.ShelveAlarmAsync(alarm, kind, duration); on Good close, else show the Bad in the result banner and stay open. "Cancel" closes.ConfirmAlarmWindow(NEW —…/Views/ConfirmAlarmWindow.axaml{,.cs}) — cloneAckAlarmWindow(comment box + result banner) →_alarmsVm.ConfirmAlarmAsync(alarm, comment). Cloning (vs. parametrizing AckAlarmWindow) keeps the codebase's explicit one-window-per-action style (~40 lines).AlarmsView.axaml.cs— in the runtime context-menu builder, add twoMenuItems: "Shelve…" (enabled/shown whenCanShelve) and "Confirm…" (whenCanConfirm), each opening its dialog for the right-clicked alarm — mirror the existingOnAcknowledgeClickedhandler + theCanAcknowledgegate.
Data flow
right-click row → context menu (items gated on Can*) → dialog collects inputs →
AlarmsViewModel.{Shelve,Confirm}AlarmAsync → IOpcUaClientService.{Shelve,Confirm}AlarmAsync →
OPC UA Part 9 method call → StatusCode → result banner; on Good close the dialog. The grid updates
through the existing alarm-event subscription (no manual row mutation).
Error handling
- Not connected / missing required ids → friendly
(false, message), NO service call (mirror Ack). - Timed shelve with
durationSeconds <= 0→ friendly fail before the call. - Service returns a Bad
StatusCode→ surface its text in the dialog result banner, keep the dialog open (mirror Ack).
Testing
- Unit (
Client.UI.Tests, mirrorAlarmsViewModelTests+ the UIFakeOpcUaClientService):ShelveAlarmAsync— connected + OneShot/Timed/Unshelve calls the service with the right kind/duration; not-connected ⇒ no call + fail; Timed with duration ≤ 0 ⇒ no call + fail; service Bad ⇒ fail message.ConfirmAlarmAsync— connected calls service with nodeId/eventId/comment; not-connected / missing id ⇒ no call + fail; service Bad ⇒ fail. Enable predicates:CanShelve/CanConfirmtruth tables. Extend the UIFakeOpcUaClientServicewithShelveAlarmCalls/ConfirmAlarmCalls+ configurable result codes (if not already present). dotnet buildclean +dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Testsgreen.- Live
/run(honest constraint): the Avalonia desktop GUI is not agent-drivable (no display; the Chrome browser tools are web-only). Live verification = build + a launch/connect smoke against the running rig (dotnet run --project src/Client/…Client.UI, connect toopc.tcp://localhost:4840which carries 1 scripted alarm) to confirm the app starts, connects, and the Alarms grid + context menu render. The actual Ack/Shelve/Confirm click-through is operator-gated — the VM-method → service round-trip is fully unit-proven. (Recorded as an honest deferral, like prior operator-gated gates.)
Out of scope
ConfirmedStateevent-parsing tracking (chose "simple").- The Galaxy sub-attribute operator-comment collapse (
stillpending.md§4 — "verify, may be acceptable") — left as a noted follow-up. - NO Client.CLI changes; NO
IOpcUaClientServicechanges; NO Commons/EF change.
Hard constraints (carried from the parent roadmap)
- Stage by path — never
git add .. Never stagesql_login.txt,src/Server/.../Host/pki/,pending.md,current.md,docker-dev/docker-compose.yml,stillpending.md. Never echo/commit secrets. No--no-verify, no force-push. NO EF migration, NO Commons contract change. Finish = merge to master + push.