diff --git a/docs/plans/2026-05-22-execution-tree-node-modal.md b/docs/plans/2026-05-22-execution-tree-node-modal.md new file mode 100644 index 0000000..9eaa82f --- /dev/null +++ b/docs/plans/2026-05-22-execution-tree-node-modal.md @@ -0,0 +1,110 @@ +# Execution-Tree Node Detail Modal — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to execute this plan task-by-task (fresh implementer per task + spec review + code-quality review). + +**Goal:** Double-clicking a node on the `/audit/execution-tree` page opens a modal listing that execution's audit rows; clicking a row shows its full detail — the same content the `/audit/log` drilldown drawer renders. + +**Architecture:** Extract the drawer's single-`AuditEvent` body into a shared `AuditEventDetail` component reused by both the drawer and a new `ExecutionDetailModal`. The `ExecutionTree` node gains a double-click that raises an `EventCallback` bubbling up the recursive instances to `ExecutionTreePage`, which hosts the modal. The modal fetches the execution's rows via the existing `IAuditLogQueryService.QueryAsync` (filter by `ExecutionId`) — no DB / repository / service-contract change. Validated design: `docs/plans/2026-05-22-execution-tree-node-modal-design.md`. + +**Tech Stack:** .NET 10, Blazor Server + Bootstrap (custom components, no component frameworks), xUnit + bUnit, Playwright. + +**Ground rules (every task):** branch is `feature/execution-tree-node-modal` (already created) — never commit to `main`. TDD — failing test first, then minimal implementation. Edit in place; never touch `infra/*` or `alog.md`; `docker/*` only if a task says so (none do). Stage with explicit `git add ` — never `git add .` / `commit -am`. Full solution stays green: `dotnet build ScadaLink.slnx` 0 warnings (`TreatWarningsAsErrors` on); `dotnet test tests/ScadaLink.CentralUI.Tests` for touched UI work. Use the `frontend-design` skill for new markup/CSS. Do not push. + +--- + +## Task 0: Prep — verify branch + baseline + +**Files:** none. + +**Steps:** confirm `git branch --show-current` is `feature/execution-tree-node-modal`; `dotnet build ScadaLink.slnx` succeeds with 0 warnings. + +**Acceptance:** on the branch, solution builds clean. + +--- + +## Task 1: Extract `AuditEventDetail` from `AuditDrilldownDrawer` + +**What:** Pull the drawer's single-`AuditEvent` body — the read-only field list, the Error/Request/Response/Extra sections, and the action buttons (Copy as cURL, Show all events, View this/parent execution, View execution chain) — into a new reusable component. The drawer keeps only its offcanvas chrome (header, the two Close buttons) and delegates its body to the new component. This is a pure refactor — no behaviour change. + +**Files:** +- Create: `src/ScadaLink.CentralUI/Components/Audit/AuditEventDetail.razor` (+ `.razor.cs`, + `.razor.css` if body-specific styles move). +- Modify: `src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor` — the `offcanvas-body` content + the action buttons in `drawer-footer` become ``. The drawer keeps the offcanvas backdrop/header, `ShortEventId`, the `drawer-close` / `drawer-close-footer` Close buttons. +- Modify: `src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor.cs` — move the body/action members to `AuditEventDetail.razor.cs`: `IsApiChannel`, `FormatTimestamp`, `IsRedacted`, `RenderBody`, `BuildSqlParameterRows`, `TryPrettyPrintJson`, `PrettyPrintJson`, `TryParseDbBody`, `StringifyJsonValue`, the `RedactionSentinel`/`RedactorErrorSentinel` consts, `CopyCurl`, `ShowAllForOperation`, `ViewThisExecution`, `ViewParentExecution`, `ViewExecutionChain`, `BuildCurlCommand`, `TryExtractCurlPartsFromJson`, `QuoteShellArg`, and the `[Inject] IJSRuntime JS` + `[Inject] NavigationManager Navigation`. The drawer keeps `Event`, `IsOpen`, `OnClose`, `ShortEventId`, `HandleClose`. +- Modify: `src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor.css` — move body-specific rules (e.g. `drawer-pre`) into `AuditEventDetail.razor.css` (Blazor scoped CSS follows the markup). Keep the `drawer-pre` class name to minimise churn. +- Test: create `tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditEventDetailTests.cs` — render `AuditEventDetail` directly; assert the field block (`data-test="field-..."`), the Error/Request/Response/Extra sections, the redaction badge, and the action buttons render for representative events. + +**Approach:** the markup moves verbatim — every existing `data-test` attribute (`drawer-fields`, `field-*`, `section-error`, `request-body`, `copy-as-curl`, `view-parent-execution`, …) must keep its exact value so the existing `AuditDrilldownDrawerTests` bUnit suite and the `/audit/log` Playwright drawer tests still pass unchanged (they render the drawer, which now contains the child — the selectors still resolve). `AuditEventDetail` takes a non-null `[Parameter] AuditEvent Event`. + +**Verify:** `dotnet build ScadaLink.slnx` (0 warnings); `dotnet test tests/ScadaLink.CentralUI.Tests` — the existing `AuditDrilldownDrawerTests` MUST still pass. + +**Commit:** `refactor(centralui): extract AuditEventDetail from AuditDrilldownDrawer` + +--- + +## Task 2: `ExecutionTree` — double-click raises `OnNodeActivated` + +**What:** A double-click anywhere on a tree node raises an `EventCallback` carrying the node's `ExecutionId`; the callback bubbles up the recursive `ExecutionTree` instances to the root. + +**Files:** +- Modify: `src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs` — add `[Parameter] public EventCallback OnNodeActivated { get; set; }`. +- Modify: `src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor` — add `@ondblclick="() => OnNodeActivated.InvokeAsync(node.ExecutionId)"` to the `execution-tree-body` div (NOT the `execution-tree-toggle` button, which keeps its own `@onclick`). Pass the callback straight down on the recursive child: `` — threaded unchanged at every depth, so a deep node's double-click invokes the same root-supplied callback. +- Modify: `src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.css` — add `user-select: none` to `.execution-tree-node` so a double-click does not leave an awkward text selection. +- Test: extend `tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionTreeTests.cs` — `DoubleClickingNode_RaisesOnNodeActivated_WithExecutionId`; `DoubleClickingNestedNode_BubblesOnNodeActivated_ToRoot` (a multi-level tree, double-click a child/grandchild node, assert the root callback fires with the right id). + +**Approach:** the short-id `` link keeps its single-click navigation untouched — double-clicking the link itself still navigates (acceptable; the link is a small target and the design keeps it as the explicit "go to grid" affordance). The double-click handler lives on the node body so double-clicking the meta area / row-count opens the modal. + +**Commit:** `feat(centralui): ExecutionTree node double-click raises OnNodeActivated` + +--- + +## Task 3: `ExecutionDetailModal` component + +**What:** A custom Bootstrap modal that, given an `ExecutionId`, loads that execution's audit rows and shows a list → per-row detail. + +**Files:** +- Create: `src/ScadaLink.CentralUI/Components/Audit/ExecutionDetailModal.razor` (+ `.razor.cs` + `.razor.css`). +- Parameters / DI: `[Parameter] Guid? ExecutionId`, `[Parameter] bool IsOpen`, `[Parameter] EventCallback OnClose`; `[Inject] IAuditLogQueryService`. +- Behaviour: when `IsOpen` flips true with a non-null `ExecutionId`, call `QueryAsync(new AuditLogQueryFilter(ExecutionId: ExecutionId.Value), new AuditLogPaging(PageSize: 100))`. Internal state: `_rows` (`IReadOnlyList`), `_selectedRow` (`AuditEvent?` — null = list view), `_loading`, `_error`. + - `_rows.Count >= 2` → list view: each row a `