# 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<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 = 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.
