docs(phase7): design — Client.UI alarm Ack/Shelve/Confirm via context menu
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user