Files
lmxopcua/docs/plans/2026-06-16-stillpending-phase-7-client-alarm-buttons-design.md
T

7.9 KiB
Raw Blame History

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). Branch feat/stillpending-phase-7-client-alarm-buttons off master ad3ec9d9. Phases 06 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 a DataGrid; its code-behind builds a runtime context menu in OnLoaded with ONE item, "Acknowledge…", shown when CanAcknowledge, opening AckAlarmWindowAlarmsViewModel.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 return Task<StatusCode>.
  • Client.CLI ack/confirm/shelve commands already complete — the reference param shapes.
  • AlarmEventViewModel exposes ConditionNodeId, EventId, ActiveState, AckedState, Retain, Severity, Message, Time, and CanAcknowledge (ActiveState && !AckedState && EventId != null && ConditionNodeId != null). NO CanShelve/CanConfirm, NO ShelvedState/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-side FakeOpcUaClientService test double). CLI side has ShelveCommandTests/ConfirmCommandTests.

Locked decisions

  1. 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.)
  2. Confirm gating = simpleCanConfirm = AckedState && EventId != null && ConditionNodeId != null. No ConfirmedState event-parsing tracking (the server rejects a redundant Confirm; the result banner surfaces the Bad). Avoids touching the Client.Shared event-mapping path.

Components

  1. AlarmsViewModel (…/ViewModels/AlarmsViewModel.cs) — add two methods mirroring AcknowledgeAlarmAsync:
    • Task<(bool Success, string Message)> ShelveAlarmAsync(AlarmEventViewModel alarm, ShelveKind kind, double durationSeconds) — guard IsConnected && alarm.ConditionNodeId != null (EventId NOT needed for shelve); guard Timed ⇒ durationSeconds > 0 (friendly fail BEFORE the call); call _service.ShelveAlarmAsync(alarm.ConditionNodeId, kind, durationSeconds); map StatusCode(Success, Message).
    • Task<(bool Success, string Message)> ConfirmAlarmAsync(AlarmEventViewModel alarm, string comment) — guard IsConnected && ConditionNodeId != null && EventId != null; call _service.ConfirmAlarmAsync(nodeId, eventId, comment); map result. (Byte-for-byte mirror of AcknowledgeAlarmAsync.)
  2. AlarmEventViewModel (…/ViewModels/AlarmEventViewModel.cs) — add computed:
    • bool CanShelve => ConditionNodeId != null;
    • bool CanConfirm => AckedState && EventId != null && ConditionNodeId != null;
  3. ShelveAlarmWindow (NEW — …/Views/ShelveAlarmWindow.axaml{,.cs}) — mirror AckAlarmWindow'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.
  4. ConfirmAlarmWindow (NEW — …/Views/ConfirmAlarmWindow.axaml{,.cs}) — clone AckAlarmWindow (comment box + result banner) → _alarmsVm.ConfirmAlarmAsync(alarm, comment). Cloning (vs. parametrizing AckAlarmWindow) keeps the codebase's explicit one-window-per-action style (~40 lines).
  5. AlarmsView.axaml.cs — in the runtime context-menu builder, add two MenuItems: "Shelve…" (enabled/shown when CanShelve) and "Confirm…" (when CanConfirm), each opening its dialog for the right-clicked alarm — mirror the existing OnAcknowledgeClicked handler + the CanAcknowledge gate.

Data flow

right-click row → context menu (items gated on Can*) → dialog collects inputs → AlarmsViewModel.{Shelve,Confirm}AlarmAsyncIOpcUaClientService.{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, mirror AlarmsViewModelTests + the UI FakeOpcUaClientService): 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 / CanConfirm truth tables. Extend the UI FakeOpcUaClientService with ShelveAlarmCalls / ConfirmAlarmCalls + configurable result codes (if not already present).
  • dotnet build clean + dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests green.
  • 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 to opc.tcp://localhost:4840 which 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

  • ConfirmedState event-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 IOpcUaClientService changes; NO Commons/EF change.

Hard constraints (carried from the parent roadmap)

  • Stage by path — never git add .. Never stage sql_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.