feat(cli): add --execution-timeout-seconds + --min-time-between-runs to template script (#54)

Expose TemplateScript.ExecutionTimeoutSeconds and MinTimeBetweenRuns (previously
settable only via Transport bundle import) on the CLI and Central UI authoring surfaces.

- Commons: add additive trailing optionals MinTimeBetweenRuns (TimeSpan?) and
  ExecutionTimeoutSeconds (int?) to Add/UpdateTemplateScriptCommand.
- ManagementActor: thread both new fields into the built TemplateScript on add/update.
- CLI template script add/update: new --min-time-between-runs (duration: ms/s/min,
  bare number = seconds, 0 = unset, mirroring DurationInput) and
  --execution-timeout-seconds (int) flags, with client-side duration validation.
- Central UI TemplateEdit: add an Execution timeout input (seconds) on the script
  trigger tab, mirroring the existing Min-time-between-runs control; null/0 = site default.
- Tests: TemplateScriptTimingTests pins the option surface + duration parsing; updated
  the stale 'no UI control' comment on the TemplateService round-trip test.
This commit is contained in:
Joseph Doherty
2026-06-19 03:14:10 -04:00
parent 5185486a3c
commit ae25b5a8d6
5 changed files with 162 additions and 18 deletions
@@ -130,8 +130,8 @@
private string? _scriptParameters;
private string? _scriptReturn;
private bool _scriptIsLocked;
// Round-tripped from the loaded script so UI edits preserve a timeout set
// via Transport import (no authoring control in the UI — scoped out).
// Per-script execution-timeout override (seconds). Bound to the "Execution timeout"
// input on the trigger tab; null/0 means "use the site's global default" (#54).
private int? _scriptExecutionTimeoutSeconds;
private string? _scriptFormError;
private string _scriptModalTab = "trigger"; // "trigger" | "code" | "parameters" | "return"
@@ -1397,6 +1397,19 @@
}
</div>
}
<div class="mt-3">
<label class="form-label">Execution timeout</label>
<div class="input-group" style="max-width: 280px;">
<input type="number" min="1" step="1" class="form-control"
placeholder="(site default)"
@bind="_scriptExecutionTimeoutSeconds" @bind:event="oninput" />
<span class="input-group-text">seconds</span>
</div>
<div class="form-text">
Per-script execution timeout. Leave blank (or 0) to use the
site's global default.
</div>
</div>
</div>
<div style="display: @(_scriptModalTab == "code" ? "block" : "none")">
<MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
@@ -2129,6 +2142,12 @@
_ => "Error"
};
// Normalizes the execution-timeout input: a null or non-positive value means
// "use the site default", so it is stored as null (matching the Site Runtime's
// own ≤0-means-default handling and the entity's documented contract).
private static int? NormalizeExecutionTimeout(int? seconds)
=> seconds is > 0 ? seconds : null;
private async Task SaveScript()
{
if (_selectedTemplate == null) return;
@@ -2150,9 +2169,7 @@
ReturnDefinition = _scriptReturn,
IsLocked = _scriptIsLocked,
MinTimeBetweenRuns = DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit),
// Round-trip the loaded value — no UI control, so preserve
// any timeout set via Transport import unchanged.
ExecutionTimeoutSeconds = _scriptExecutionTimeoutSeconds,
ExecutionTimeoutSeconds = NormalizeExecutionTimeout(_scriptExecutionTimeoutSeconds),
IsInherited = existing.IsInherited,
LockedInDerived = existing.LockedInDerived,
};
@@ -2178,7 +2195,8 @@
ParameterDefinitions = _scriptParameters,
ReturnDefinition = _scriptReturn,
IsLocked = _scriptIsLocked,
MinTimeBetweenRuns = DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit)
MinTimeBetweenRuns = DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit),
ExecutionTimeoutSeconds = NormalizeExecutionTimeout(_scriptExecutionTimeoutSeconds)
};
var addResult = await TemplateService.AddScriptAsync(_selectedTemplate.Id, script, user);
@@ -29,8 +29,13 @@ public record AddTemplateNativeAlarmSourceCommand(int TemplateId, string Name, s
public record UpdateTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked);
public record DeleteTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId);
public record ListTemplateNativeAlarmSourcesCommand(int TemplateId);
public record AddTemplateScriptCommand(int TemplateId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked, string? ParameterDefinitions = null, string? ReturnDefinition = null);
public record UpdateTemplateScriptCommand(int ScriptId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked, string? ParameterDefinitions = null, string? ReturnDefinition = null);
// MinTimeBetweenRuns/ExecutionTimeoutSeconds are additive trailing optionals (default null = "unset"):
// MinTimeBetweenRuns mirrors the TemplateScript.MinTimeBetweenRuns TimeSpan throttle/re-fire interval;
// ExecutionTimeoutSeconds mirrors the per-script TemplateScript.ExecutionTimeoutSeconds override (seconds;
// null/non-positive falls back to the site's global default). Both were previously settable only via
// Transport bundle import — these fields close the CLI/UI authoring gap (#54). Additive-only: never reorder.
public record AddTemplateScriptCommand(int TemplateId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked, string? ParameterDefinitions = null, string? ReturnDefinition = null, TimeSpan? MinTimeBetweenRuns = null, int? ExecutionTimeoutSeconds = null);
public record UpdateTemplateScriptCommand(int ScriptId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked, string? ParameterDefinitions = null, string? ReturnDefinition = null, TimeSpan? MinTimeBetweenRuns = null, int? ExecutionTimeoutSeconds = null);
public record DeleteTemplateScriptCommand(int ScriptId);
public record AddTemplateCompositionCommand(int TemplateId, string InstanceName, int ComposedTemplateId);
public record DeleteTemplateCompositionCommand(int CompositionId);
@@ -2170,7 +2170,9 @@ public class ManagementActor : ReceiveActor
TriggerConfiguration = cmd.TriggerConfiguration,
IsLocked = cmd.IsLocked,
ParameterDefinitions = cmd.ParameterDefinitions,
ReturnDefinition = cmd.ReturnDefinition
ReturnDefinition = cmd.ReturnDefinition,
MinTimeBetweenRuns = cmd.MinTimeBetweenRuns,
ExecutionTimeoutSeconds = cmd.ExecutionTimeoutSeconds
};
var result = await svc.AddScriptAsync(cmd.TemplateId, script, user);
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
@@ -2185,7 +2187,9 @@ public class ManagementActor : ReceiveActor
TriggerConfiguration = cmd.TriggerConfiguration,
IsLocked = cmd.IsLocked,
ParameterDefinitions = cmd.ParameterDefinitions,
ReturnDefinition = cmd.ReturnDefinition
ReturnDefinition = cmd.ReturnDefinition,
MinTimeBetweenRuns = cmd.MinTimeBetweenRuns,
ExecutionTimeoutSeconds = cmd.ExecutionTimeoutSeconds
};
var result = await svc.UpdateScriptAsync(cmd.ScriptId, script, user);
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);