From 66d5a67e4f76d602319fcef5dae4d4d7c6b1d72f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 6 Sep 2026 00:33:37 -0400 Subject: [PATCH] feat(central-ui): Flow Debugger drill-ins from Audit Log + Execution Chain; TraceAllCalls checkbox on the method form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'Open in Flow Debugger' (data-test open-flow-debugger) beside 'View execution chain' in AuditEventDetail (rendered only with an ExecutionId) and beside 'View this execution in the Audit Log' on the Execution Chain page — both target /deployment/flow-debugger?executionId=… which replays on first render. ApiMethodForm gains a 'Trace all calls (Flow Debugger)' form-check under Timeout, loaded from and saved to ApiMethod.TraceAllCalls on both the update and create paths, with the ~7-rows-per-call guidance as help text. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP --- .../Components/Audit/AuditEventDetail.razor | 5 + .../Audit/AuditEventDetail.razor.cs | 10 ++ .../Pages/Audit/ExecutionTreePage.razor | 7 +- .../Pages/Design/ApiMethodForm.razor | 18 +++- .../Audit/FlowDebuggerDrillinTests.cs | 97 +++++++++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Audit/FlowDebuggerDrillinTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor index dafa5285..b77be965 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor @@ -164,5 +164,10 @@ @onclick="ViewExecutionChain"> View execution chain + } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor.cs index 0ab4ca30..7178b83c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditEventDetail.razor.cs @@ -295,6 +295,16 @@ public partial class AuditEventDetail Navigation.NavigateTo(uri); } + /// + /// Flow Debugger replay drill-in (design 2026-09-05 §4): rebuilds this execution's + /// whole flow — inbound row, route call, traced site steps — as a swimlane. + /// + private void OpenInFlowDebugger() + { + if (Event.ExecutionId is not { } exec) return; + Navigation.NavigateTo($"/deployment/flow-debugger?executionId={exec}"); + } + /// /// Build a cURL command from an audit event. The URL comes from /// Target; when the RequestSummary parses as diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor index a4e12f3d..bf8dfc51 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor @@ -39,12 +39,17 @@ } else if (_nodes is { Count: > 0 }) { -
+ diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ApiMethodForm.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ApiMethodForm.razor index 4cbe605b..35b1bda8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ApiMethodForm.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ApiMethodForm.razor @@ -35,10 +35,22 @@
-
+
+
+ + +
+ Every call to this method records its routed site steps as audit rows the + Flow Debugger can replay. Adds ~7 audit rows per call; for low-volume + integration methods. +
+
@@ -211,6 +223,7 @@ private bool _loading = true; private string _name = "", _script = ""; private int _timeoutSeconds = 30; + private bool _traceAllCalls; private string? _params, _returns; private string? _formError; private MonacoEditor? _editor; @@ -253,6 +266,7 @@ _name = _existing.Name; _script = _existing.Script; _timeoutSeconds = _existing.TimeoutSeconds; + _traceAllCalls = _existing.TraceAllCalls; _params = _existing.ParameterDefinitions; _returns = _existing.ReturnDefinition; // Seed approved keys from the seam: which keys' scopes contain this method. @@ -291,6 +305,7 @@ { _existing.Script = _script; _existing.TimeoutSeconds = _timeoutSeconds; + _existing.TraceAllCalls = _traceAllCalls; _existing.ParameterDefinitions = _params?.Trim(); _existing.ReturnDefinition = _returns?.Trim(); await InboundApiRepository.UpdateApiMethodAsync(_existing); @@ -301,6 +316,7 @@ var m = new ApiMethod(_name.Trim(), _script) { TimeoutSeconds = _timeoutSeconds, + TraceAllCalls = _traceAllCalls, ParameterDefinitions = _params?.Trim(), ReturnDefinition = _returns?.Trim() }; diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Audit/FlowDebuggerDrillinTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Audit/FlowDebuggerDrillinTests.cs new file mode 100644 index 00000000..d4734460 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Audit/FlowDebuggerDrillinTests.cs @@ -0,0 +1,97 @@ +using Bunit; +using Bunit.TestDoubles; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit; +using ZB.MOM.WW.ScadaBridge.CentralUI.Services; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Audit; + +/// +/// Flow Debugger drill-ins (design 2026-09-05 §4, plan Task 21): the audit drawer's +/// detail body and the Execution Chain page both offer Open in Flow Debugger, +/// and the inbound-method form carries the TraceAllCalls checkbox wired to both +/// save paths. +/// +public class FlowDebuggerDrillinTests : BunitContext +{ + public FlowDebuggerDrillinTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + } + + private static AuditEventView MakeEvent(Guid? executionId) => new() + { + EventId = Guid.Parse("11111111-2222-3333-4444-555555555555"), + OccurredAtUtc = new DateTime(2026, 9, 5, 12, 30, 45, DateTimeKind.Utc), + IngestedAtUtc = new DateTime(2026, 9, 5, 12, 30, 46, DateTimeKind.Utc), + Channel = AuditChannel.ApiInbound, + Kind = AuditKind.InboundRequest, + ExecutionId = executionId, + Actor = "tester", + Target = "POST /api/DelmiaRecipeDownload", + Status = AuditStatus.Delivered, + DurationMs = 42, + }; + + private static string SrcRoot + { + get + { + var dir = AppContext.BaseDirectory; + for (var i = 0; i < 6 && dir is not null; i++) + dir = Directory.GetParent(dir)?.FullName; + return Path.Combine(dir!, "src", "ZB.MOM.WW.ScadaBridge.CentralUI", "Components"); + } + } + + private static string Read(params string[] relative) + => File.ReadAllText(Path.Combine([SrcRoot, .. relative])); + + [Fact] + public void AuditEventDetail_WithExecutionId_OffersOpenInFlowDebugger_AndNavigatesToReplay() + { + var exec = Guid.Parse("12345678-aaaa-bbbb-cccc-1234567890ab"); + var cut = Render(p => p.Add(c => c.Event, MakeEvent(exec))); + + cut.Find("[data-test=\"open-flow-debugger\"]").Click(); + + var nav = (BunitNavigationManager)Services.GetRequiredService(); + Assert.Contains($"/deployment/flow-debugger?executionId={exec}", nav.Uri); + } + + [Fact] + public void AuditEventDetail_WithoutExecutionId_HidesTheButton() + { + var cut = Render(p => p.Add(c => c.Event, MakeEvent(null))); + + Assert.DoesNotContain("data-test=\"open-flow-debugger\"", cut.Markup); + } + + [Fact] + public void ExecutionTreePage_LinksToTheFlowDebuggerReplay_BesideTheAuditLogLink() + { + var markup = Read("Pages", "Audit", "ExecutionTreePage.razor"); + + Assert.Contains("data-test=\"open-flow-debugger\"", markup); + Assert.Contains("/deployment/flow-debugger?executionId={_executionId}", markup); + // Same block as the existing back-to-log link, so both actions sit together. + Assert.True(markup.IndexOf("execution-tree-back-to-log", StringComparison.Ordinal) + < markup.IndexOf("open-flow-debugger", StringComparison.Ordinal)); + } + + [Fact] + public void ApiMethodForm_HasTraceAllCallsCheckbox_WiredToBothSavePaths() + { + var markup = Read("Pages", "Design", "ApiMethodForm.razor"); + + Assert.Contains("data-test=\"api-method-trace-all-calls\"", markup); + Assert.Contains("Trace all calls (Flow Debugger)", markup); + Assert.Contains("~7 audit rows per call", markup); + // Load, update and create paths all carry the flag. + Assert.Contains("_traceAllCalls = _existing.TraceAllCalls;", markup); + Assert.Contains("_existing.TraceAllCalls = _traceAllCalls;", markup); + Assert.Contains("TraceAllCalls = _traceAllCalls,", markup); + } +}