# 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 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 a `DataGrid`; its code-behind builds a **runtime context menu in `OnLoaded`** with ONE item, "Acknowledge…", shown when `CanAcknowledge`, opening `AckAlarmWindow` → `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 return `Task`. - **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 = simple** — `CanConfirm = 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 `MenuItem`s: "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}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`, 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.