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

117 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `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.