diff --git a/docs/plans/2026-06-16-stillpending-phase-7-client-alarm-buttons.md b/docs/plans/2026-06-16-stillpending-phase-7-client-alarm-buttons.md
new file mode 100644
index 00000000..2f8d4dc4
--- /dev/null
+++ b/docs/plans/2026-06-16-stillpending-phase-7-client-alarm-buttons.md
@@ -0,0 +1,494 @@
+# Phase 7 — Client.UI Alarm Ack/Shelve/Confirm Buttons — Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans (or subagent-driven-development) to implement this plan task-by-task. Read this plan + `docs/plans/2026-06-16-stillpending-phase-7-client-alarm-buttons-design.md` first. Branch `feat/stillpending-phase-7-client-alarm-buttons` (off master `ad3ec9d9`) already exists with the design committed (`573728b5`).
+
+**Goal:** Give the Avalonia desktop client per-alarm **Acknowledge / Shelve / Confirm** via the existing right-click context menu, backed by the already-implemented `IOpcUaClientService` methods.
+
+**Architecture:** Avalonia + CommunityToolkit.Mvvm. Two new `AlarmsViewModel` async methods (`ShelveAlarmAsync`, `ConfirmAlarmAsync`) mirroring the existing `AcknowledgeAlarmAsync`; two new `AlarmEventViewModel` enable predicates; two new dialog windows (`ShelveAlarmWindow`, `ConfirmAlarmWindow`); and the runtime context-menu builder in `AlarmsView.axaml.cs` extended to 3 items with per-item enablement. The VM methods + predicates are unit-tested against the UI `FakeOpcUaClientService`; the dialogs + menu are proven by build + a launch/connect smoke.
+
+**Tech Stack:** C#/.NET 10, Avalonia, CommunityToolkit.Mvvm, OPCFoundation `Opc.Ua` (`StatusCode`), xUnit + Shouldly.
+
+**Global rules (every task):** TDD red→green for the VM methods + predicates. 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`. No `--no-verify`, no force-push. **NO EF migration, NO Commons/`IOpcUaClientService`/CLI contract change** (the service methods already exist). If `git commit` hits `.git/index.lock`, wait 2s + retry up to 5×.
+
+**Dependency graph:** Task 1 → Task 2 → {Task 3 ∥ Task 4} → Task 5 → Task 6 → Task 7 → Task 8.
+
+---
+
+### Task 0: Feature branch *(done)*
+Branch `feat/stillpending-phase-7-client-alarm-buttons` off `ad3ec9d9`; design committed `573728b5`. No action.
+
+---
+
+### Task 1: Extend the UI `FakeOpcUaClientService` with Shelve/Confirm call-tracking
+
+**Classification:** small · **Est:** ~3 min · **Parallelizable with:** none
+
+**Files:**
+- Modify: `tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/Fakes/FakeOpcUaClientService.cs`
+
+**Context:** The fake already implements `ConfirmAlarmAsync`/`ShelveAlarmAsync` returning `ConfirmResult`/`ShelveResult`, but does NOT track call args or support a thrown exception (unlike `AcknowledgeAlarmAsync` which has `AcknowledgeCallCount` + `AcknowledgeException`). Add the parallel tracking so the Task-2 VM tests can assert the service was called with the right args.
+
+**Step 1: Add tracking fields + exception props** (near the existing `ConfirmResult` / `ShelveResult`):
+```csharp
+/// Gets or sets the exception thrown to simulate alarm confirmation failures in the UI.
+public Exception? ConfirmException { get; set; }
+/// Gets the number of times ConfirmAlarmAsync has been called.
+public int ConfirmCallCount { get; private set; }
+/// Gets the (conditionNodeId, eventId, comment) of the last ConfirmAlarmAsync call.
+public (string ConditionNodeId, byte[] EventId, string Comment)? LastConfirmCall { get; private set; }
+
+/// Gets or sets the exception thrown to simulate alarm shelve failures in the UI.
+public Exception? ShelveException { get; set; }
+/// Gets the number of times ShelveAlarmAsync has been called.
+public int ShelveCallCount { get; private set; }
+/// Gets the (conditionNodeId, kind, shelvingTimeSeconds) of the last ShelveAlarmAsync call.
+public (string ConditionNodeId, ShelveKind Kind, double ShelvingTimeSeconds)? LastShelveCall { get; private set; }
+```
+
+**Step 2: Update the two methods** to record + honor the exception (mirror `AcknowledgeAlarmAsync`):
+```csharp
+public Task ConfirmAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
+ CancellationToken ct = default)
+{
+ ConfirmCallCount++;
+ LastConfirmCall = (conditionNodeId, eventId, comment);
+ if (ConfirmException != null) throw ConfirmException;
+ return Task.FromResult(ConfirmResult);
+}
+
+public Task ShelveAlarmAsync(string conditionNodeId, ShelveKind kind, double shelvingTimeSeconds = 0,
+ CancellationToken ct = default)
+{
+ ShelveCallCount++;
+ LastShelveCall = (conditionNodeId, kind, shelvingTimeSeconds);
+ if (ShelveException != null) throw ShelveException;
+ return Task.FromResult(ShelveResult);
+}
+```
+(`ShelveKind` is already imported via `using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;` at the top.)
+
+**Step 3: Build the test project** — `dotnet build tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests` → 0 errors (no test consumes the new members yet; this just confirms it compiles).
+
+**Step 4: Commit**
+```bash
+git add tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/Fakes/FakeOpcUaClientService.cs
+git commit -m "test(client-ui): track Shelve/Confirm calls + exceptions in FakeOpcUaClientService"
+```
+
+---
+
+### Task 2: `AlarmsViewModel.ShelveAlarmAsync` + `ConfirmAlarmAsync` + `CanShelve`/`CanConfirm` (TDD)
+
+**Classification:** standard · **Est:** ~5 min · **Parallelizable with:** none · **blockedBy:** Task 1
+
+**Files:**
+- Modify: `src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs` (add 2 methods after `AcknowledgeAlarmAsync` ~:193)
+- Modify: `src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs` (add 2 predicates after `CanAcknowledge` :98)
+- Test: `tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs`
+
+**Step 1: Write the failing tests.** Read the existing `AlarmsViewModelTests.cs` first to copy its setup (how the `AlarmsViewModel` is constructed with the `FakeOpcUaClientService`, how `IsConnected` is set, how an `AlarmEventViewModel` is built — mirror the existing Acknowledge tests exactly). Add these cases (names indicative; match the file's naming style):
+
+```csharp
+// --- Shelve ---
+// connected + OneShot: calls service with kind=OneShot, returns success
+// _service.IsConnected = true; _vm... ; var alarm = new AlarmEventViewModel(... conditionNodeId: "ns=2;s=A", eventId: new byte[]{1});
+// var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
+// ok.ShouldBeTrue(); _service.ShelveCallCount.ShouldBe(1);
+// _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.OneShot);
+// _service.LastShelveCall!.Value.ConditionNodeId.ShouldBe("ns=2;s=A");
+// connected + Timed + duration 300: passes ShelvingTimeSeconds=300
+// connected + Unshelve: kind=Unshelve, success
+// Timed + duration <= 0: returns (false, ...), ShelveCallCount==0 (no service call)
+// not connected: returns (false, ...), ShelveCallCount==0
+// missing ConditionNodeId (null): returns (false, ...), ShelveCallCount==0
+// service returns Bad (_service.ShelveResult = StatusCodes.BadNodeIdUnknown): returns (false, message contains "Shelve failed")
+
+// --- Confirm ---
+// connected: calls service with nodeId+eventId+comment, returns success; ConfirmCallCount==1; LastConfirmCall comment matches
+// not connected: (false, ...), ConfirmCallCount==0
+// missing EventId (null): (false, ...), ConfirmCallCount==0
+// service Bad (_service.ConfirmResult = StatusCodes.BadNodeIdUnknown): (false, message contains "Confirm failed")
+
+// --- Predicates (pure, construct AlarmEventViewModel directly) ---
+// CanShelve: true when ConditionNodeId != null; false when null
+// CanConfirm: true when AckedState && EventId != null && ConditionNodeId != null; false when not acked / null event / null node
+```
+
+**Step 2: Run → FAIL** (`ShelveAlarmAsync`/`ConfirmAlarmAsync`/`CanShelve`/`CanConfirm` not defined):
+`dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests --filter "FullyQualifiedName~AlarmsViewModel"` → compile error / fail.
+
+**Step 3: Implement.** In `AlarmsViewModel.cs` (ensure `using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;` is present for `ShelveKind` — add it if missing), after `AcknowledgeAlarmAsync`:
+```csharp
+/// Shelves / unshelves an alarm and returns (success, message).
+public async Task<(bool Success, string Message)> ShelveAlarmAsync(
+ AlarmEventViewModel alarm, ShelveKind kind, double durationSeconds)
+{
+ if (!IsConnected || alarm.ConditionNodeId == null)
+ return (false, "Alarm cannot be shelved (not connected or missing ConditionId).");
+ if (kind == ShelveKind.Timed && durationSeconds <= 0)
+ return (false, "Timed shelve requires a positive duration (seconds).");
+
+ try
+ {
+ var result = await _service.ShelveAlarmAsync(alarm.ConditionNodeId, kind, durationSeconds);
+ if (Opc.Ua.StatusCode.IsGood(result))
+ return (true, $"Alarm {kind.ToString().ToLowerInvariant()} succeeded.");
+ return (false, $"Shelve failed: {Helpers.StatusCodeFormatter.Format(result)}");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"Error: {ex.Message}");
+ }
+}
+
+/// Confirms an alarm and returns (success, message).
+public async Task<(bool Success, string Message)> ConfirmAlarmAsync(AlarmEventViewModel alarm, string comment)
+{
+ if (!IsConnected || alarm.EventId == null || alarm.ConditionNodeId == null)
+ return (false, "Alarm cannot be confirmed (missing EventId or ConditionId).");
+
+ try
+ {
+ var result = await _service.ConfirmAlarmAsync(alarm.ConditionNodeId, alarm.EventId, comment);
+ if (Opc.Ua.StatusCode.IsGood(result))
+ return (true, "Alarm confirmed successfully.");
+ return (false, $"Confirm failed: {Helpers.StatusCodeFormatter.Format(result)}");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"Error: {ex.Message}");
+ }
+}
+```
+In `AlarmEventViewModel.cs`, after `CanAcknowledge` (:98):
+```csharp
+/// Whether this alarm can be shelved/unshelved (has a ConditionNodeId).
+public bool CanShelve => ConditionNodeId != null;
+
+/// Whether this alarm can be confirmed (already acked, has EventId + ConditionNodeId).
+public bool CanConfirm => AckedState && EventId != null && ConditionNodeId != null;
+```
+
+**Step 4: Run → PASS.** `dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests --filter "FullyQualifiedName~AlarmsViewModel"` → green. Then `dotnet build src/Client/ZB.MOM.WW.OtOpcUa.Client.UI` → 0 errors.
+
+**Step 5: Commit**
+```bash
+git add src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs \
+ src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs \
+ tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs
+git commit -m "feat(client-ui): AlarmsViewModel Shelve/Confirm methods + CanShelve/CanConfirm"
+```
+
+---
+
+### Task 3: `ShelveAlarmWindow` dialog (view + code-behind)
+
+**Classification:** small · **Est:** ~5 min · **Parallelizable with:** Task 4 · **blockedBy:** Task 2
+
+**Files:**
+- Create: `src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml`
+- Create: `src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml.cs`
+
+**Context:** Model on `AckAlarmWindow.axaml{,.cs}` (read both first). Difference: instead of a comment box, a **ShelveKind selector** (ComboBox) + a **Duration (seconds)** input enabled only for Timed. No comment (the service `ShelveAlarmAsync` takes none).
+
+**Step 1: `ShelveAlarmWindow.axaml`**
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ OneShot
+ Timed
+ Unshelve
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Step 2: `ShelveAlarmWindow.axaml.cs`** (mirror `AckAlarmWindow.axaml.cs`):
+```csharp
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
+using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
+
+namespace ZB.MOM.WW.OtOpcUa.Client.UI.Views;
+
+public partial class ShelveAlarmWindow : Window
+{
+ private readonly AlarmsViewModel _alarmsVm;
+ private readonly AlarmEventViewModel _alarm;
+
+ /// Designer ctor.
+ public ShelveAlarmWindow()
+ {
+ InitializeComponent();
+ _alarmsVm = null!;
+ _alarm = null!;
+ }
+
+ /// Creates the shelve dialog for an alarm.
+ public ShelveAlarmWindow(AlarmsViewModel alarmsVm, AlarmEventViewModel alarm)
+ {
+ InitializeComponent();
+ _alarmsVm = alarmsVm;
+ _alarm = alarm;
+
+ var sourceText = this.FindControl("SourceText");
+ if (sourceText != null) sourceText.Text = alarm.SourceName;
+
+ var conditionText = this.FindControl("ConditionText");
+ if (conditionText != null) conditionText.Text = $"{alarm.ConditionName} (Severity: {alarm.Severity})";
+
+ var kindCombo = this.FindControl("KindCombo");
+ if (kindCombo != null) kindCombo.SelectionChanged += OnKindChanged;
+
+ var shelveButton = this.FindControl