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.cssif body-specific styles move). - Modify:
src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor— theoffcanvas-bodycontent + the action buttons indrawer-footerbecome<AuditEventDetail Event="Event" />. The drawer keeps the offcanvas backdrop/header,ShortEventId, thedrawer-close/drawer-close-footerClose buttons. - Modify:
src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor.cs— move the body/action members toAuditEventDetail.razor.cs:IsApiChannel,FormatTimestamp,IsRedacted,RenderBody,BuildSqlParameterRows,TryPrettyPrintJson,PrettyPrintJson,TryParseDbBody,StringifyJsonValue, theRedactionSentinel/RedactorErrorSentinelconsts,CopyCurl,ShowAllForOperation,ViewThisExecution,ViewParentExecution,ViewExecutionChain,BuildCurlCommand,TryExtractCurlPartsFromJson,QuoteShellArg, and the[Inject] IJSRuntime JS+[Inject] NavigationManager Navigation. The drawer keepsEvent,IsOpen,OnClose,ShortEventId,HandleClose. - Modify:
src/ScadaLink.CentralUI/Components/Audit/AuditDrilldownDrawer.razor.css— move body-specific rules (e.g.drawer-pre) intoAuditEventDetail.razor.css(Blazor scoped CSS follows the markup). Keep thedrawer-preclass name to minimise churn. - Test: create
tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditEventDetailTests.cs— renderAuditEventDetaildirectly; 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 theexecution-tree-bodydiv (NOT theexecution-tree-togglebutton, 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— adduser-select: noneto.execution-tree-nodeso 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 <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
IsOpenflips true with a non-nullExecutionId, callQueryAsync(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>showingKind/Status/Target/ time; click → set_selectedRow._rows.Count == 1→ set_selectedRowto 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 amodal-backdrop), shown via theIsOpenbool +d-block/showclasses — the same hand-rolled approachAuditDrilldownDraweruses foroffcanvas, no JS framework. Header:Execution {short-id}+ row count. Close via header X, backdrop click, footer Close.data-testhooks: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 fakeIAuditLogQueryService: multi-row → list renders, row click →AuditEventDetailshown; single-row → opens straight to detail; zero-row → empty state; query throws → error state; close raisesOnClose.
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— passOnNodeActivated="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'sdata-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 (reuseAuditDataSeeder), open/audit/execution-tree?executionId=<id>, double-click a multi-row node, assert the modal opens with the row list, click a row, assert theAuditEventDetailfield 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 modifyalog.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.