Files
scadalink-design/docs/plans/2026-05-22-execution-tree-node-modal.md

11 KiB

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<Guid> 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 <path> — 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 <AuditEventDetail Event="Event" />. 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<Guid> 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<Guid> 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: <ExecutionTree ... OnNodeActivated="OnNodeActivated" /> — 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.csDoubleClickingNode_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 <a> 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<AuditEvent>), _selectedRow (AuditEvent? — null = list view), _loading, _error.
    • _rows.Count >= 2 → list view: each row a <button> showing Kind / Status / Target / time; click → set _selectedRow.
    • _rows.Count == 1 → set _selectedRow to that row on load (opens straight to detail).
    • _rows.Count == 0 → friendly empty state ("This execution emitted no audit rows.").
    • Detail view renders <AuditEventDetail Event="_selectedRow" /> plus a "← Back to rows" control (hidden / disabled when there is only one row — nothing to go back to).
    • Query failure → inline error state inside the modal; never rethrow (mirror ExecutionTreePage.LoadChainAsync's try/catch).
  • Markup: hand-rolled Bootstrap modal (modal, modal-dialog, modal-content, modal-header/modal-body/modal-footer, plus a modal-backdrop), shown via the IsOpen bool + d-block/show classes — the same hand-rolled approach AuditDrilldownDrawer uses for offcanvas, no JS framework. Header: Execution {short-id} + row count. Close via header X, backdrop click, footer Close. data-test hooks: execution-detail-modal, execution-detail-backdrop, execution-detail-close, execution-detail-row-{EventId}, execution-detail-back, execution-detail-empty, execution-detail-error.
  • Test: create tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionDetailModalTests.cs — with a fake IAuditLogQueryService: multi-row → list renders, row click → AuditEventDetail shown; single-row → opens straight to detail; zero-row → empty state; query throws → error state; close raises OnClose.

Use the frontend-design skill for the modal markup/CSS — clean corporate aesthetic, consistent with the existing Audit UI.

Commit: feat(centralui): ExecutionDetailModal — execution rows with per-row detail


Task 4: Wire the modal into ExecutionTreePage

Files:

  • Modify: src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor — pass OnNodeActivated="HandleNodeActivated" to <ExecutionTree>; add <ExecutionDetailModal ExecutionId="_modalExecutionId" IsOpen="_modalOpen" OnClose="HandleModalClose" />.
  • Modify: src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor.cs — add _modalExecutionId (Guid?), _modalOpen (bool), HandleNodeActivated(Guid executionId) (sets both + opens), HandleModalClose() (clears _modalOpen).
  • Test: extend tests/ScadaLink.CentralUI.Tests/Pages/ExecutionTreePageTests.cs — double-clicking a rendered tree node opens the modal (the modal's data-test="execution-detail-modal" appears); closing it hides the modal.

Commit: feat(centralui): open ExecutionDetailModal on tree-node double-click


Task 5: End-to-end Playwright test + docs

Files:

  • Create/extend: tests/ScadaLink.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs (or a sibling Audit Playwright file) — DoubleClickTreeNode_OpensExecutionRowModal: seed a chain (reuse AuditDataSeeder), open /audit/execution-tree?executionId=<id>, double-click a multi-row node, assert the modal opens with the row list, click a row, assert the AuditEventDetail field block shows, close the modal. Build the Playwright project; run if the cluster is available (note if skipped).
  • Modify: docs/requirements/Component-AuditLog.md — one sentence in the Central UI / Interactions section noting the execution-tree node opens a detail modal of the execution's rows. (Do NOT modify alog.md.)

Commit: test(centralui): e2e execution-tree node detail modal + docs


Final review

Dispatch a final cross-cutting review of the whole branch; full dotnet build ScadaLink.slnx (0 warnings) + dotnet test ScadaLink.slnx; hand back to the user for the push/merge/redeploy decision (do not push).

Dependency summary

0 blocks all. 1 ← 0. 2 ← 0. 3 ← 1. 4 ← 2, 3. 5 ← 4. Execution order: 0 → 1 → 2 → 3 → 4 → 5 → final review.