diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs index 26eca87a..b5bcf529 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs @@ -3,6 +3,8 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ZB.MOM.WW.Audit; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi; @@ -211,6 +213,22 @@ public sealed class FlowDebugRunner .GetSiteByIdAsync(instance.SiteId, ct).ConfigureAwait(false) ?? throw new ManagementCommandException($"Site with ID {instance.SiteId} not found.", NotFoundCode); + return await WriteAndPollAsync( + sp, user, instance, site, cmd.AttributeName, cmd.Value, cmd.WaitSeconds, triggerScript: null, ct) + .ConfigureAwait(false); + } + + /// + /// The traced attribute write + spawned-run poll shared by and + /// the write-kind branch of the script entry's RunScriptAsync (design 2026-09-08 §4.3). The + /// caller has already gated the active node, loaded and scope-checked the instance, and loaded the + /// site. is null for the attribute-level command and the script's + /// canonical name for the script entry — it only changes the NO_TRIGGER message. + /// + private async Task WriteAndPollAsync( + IServiceProvider sp, AuthenticatedUser user, Instance instance, Site site, + string attributeName, string? value, int waitSeconds, string? triggerScript, CancellationToken ct) + { var root = Guid.NewGuid(); var startedAt = DateTimeOffset.UtcNow; var sw = Stopwatch.StartNew(); @@ -223,7 +241,7 @@ public sealed class FlowDebugRunner site.SiteIdentifier, new RouteToSetAttributesRequest( Guid.NewGuid().ToString(), instance.UniqueName, - new Dictionary { [cmd.AttributeName] = cmd.Value ?? string.Empty }, + new Dictionary { [attributeName] = value ?? string.Empty }, DateTimeOffset.UtcNow, ParentExecutionId: root, Trace: true), ct).ConfigureAwait(false); @@ -232,18 +250,18 @@ public sealed class FlowDebugRunner write.Success ? AuditStatus.Delivered : AuditStatus.Failed, occurredAtUtc: startedAt.UtcDateTime, actor: user.Username, - target: cmd.AttributeName, + target: attributeName, sourceNode: sp.GetService()?.NodeName, executionId: root, sourceSiteId: site.SiteIdentifier, sourceInstanceId: instance.UniqueName, durationMs: (int)Math.Min(sw.ElapsedMilliseconds, int.MaxValue), errorMessage: write.ErrorMessage, - requestSummary: JsonSerializer.Serialize(new { attribute = cmd.AttributeName, value = cmd.Value }, ScriptTraceExtra.JsonOptions), + requestSummary: JsonSerializer.Serialize(new { attribute = attributeName, value = value }, ScriptTraceExtra.JsonOptions), extra: new ScriptTraceExtra { Source = SourceTag, - Writes = [new AttributeWriteDetail(cmd.AttributeName, cmd.Value, write.Success, write.ErrorMessage)], + Writes = [new AttributeWriteDetail(attributeName, value, write.Success, write.ErrorMessage)], }.ToJson()); await WriteCentralAsync(sp, rootRow, ct).ConfigureAwait(false); var redactedRoot = redactor?.Apply(rootRow) ?? rootRow; @@ -258,7 +276,7 @@ public sealed class FlowDebugRunner // == root, then their children — until a ScriptRun parented to root exists AND the // set has been quiet for SettleWindow, or WaitSeconds elapses. var puller = sp.GetRequiredService(); - var deadline = _now() + TimeSpan.FromSeconds(Math.Max(1, cmd.WaitSeconds)); + var deadline = _now() + TimeSpan.FromSeconds(Math.Max(1, waitSeconds)); var known = new Dictionary(); DateTimeOffset? lastNew = null; var since = startedAt.UtcDateTime.AddSeconds(-5); @@ -298,7 +316,9 @@ public sealed class FlowDebugRunner }); return new FlowDebugRunResult( root, completed, - completed ? null : $"No traced run was spawned within {cmd.WaitSeconds}s (no value-change trigger on '{cmd.AttributeName}', or no device echo).", + completed ? null : triggerScript is null + ? $"No traced run was spawned within {waitSeconds}s (no value-change trigger on '{attributeName}', or no device echo)." + : $"No traced run of '{triggerScript}' was spawned within {waitSeconds}s after writing '{attributeName}' (trigger condition not met, or no device echo).", completed ? null : NoTriggerCode, ordered, false, startedAt, DateTimeOffset.UtcNow); }