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);
}