fix(adminui,client): error boundary inside the interactive scope, stale-commit OnSaved, AbCip browse delay emit, DisposeBridge cancel (review R3)
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed. m1: DisposeBridge cancels its CTS when the 5 s wait times out. m2: a CSV import that commits after the modal closed still raises OnSaved. m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md. m4: browsePageDelayMs written only when loaded or changed; negative flagged. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+36
-6
@@ -49,7 +49,11 @@
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="abcipBrowsePageDelay">Browse page delay (ms)</label>
|
||||
<InputNumber @bind-Value="_form.BrowsePageDelayMs" @bind-Value:after="EmitAsync" class="form-control form-control-sm" id="abcipBrowsePageDelay" />
|
||||
<div class="form-text">Pause between controller symbol-browse pages (<code>browsePageDelayMs</code>). Default 5 ms; 0 disables the pause.</div>
|
||||
<div class="form-text">Pause between controller symbol-browse pages (<code>browsePageDelayMs</code>). Default 5 ms; 0 disables the pause. Left at the default on a config that never set it, the key is not written.</div>
|
||||
@if (_form.BrowsePageDelayError is { } browseDelayError)
|
||||
{
|
||||
<div class="text-danger small mt-1" id="abcipBrowsePageDelayError">@browseDelayError</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,14 +185,29 @@
|
||||
/// Devices array forward (the device form authors the real endpoints; the merge reconstructs the array
|
||||
/// from the device rows at deploy/probe time).</summary>
|
||||
public string GetConfigJson()
|
||||
=> Serialize(_form.ToOptions(_preservedDevices), _originalJson);
|
||||
=> Serialize(_form.ToOptions(_preservedDevices), _originalJson, _form.EmitsBrowsePageDelay);
|
||||
|
||||
/// <summary>Serializes options with the form's JSON settings (camelCase, enums as strings).</summary>
|
||||
internal static string Serialize(AbCipDriverOptions options, string? originalJson = null)
|
||||
=> DriverConfigJsonOverlay.Apply(originalJson, JsonSerializer.Serialize(options, _jsonOpts), OwnedPaths, TimeSpanToMs);
|
||||
/// <param name="options">The form's options.</param>
|
||||
/// <param name="originalJson">The stored config, so unmodelled keys survive.</param>
|
||||
/// <param name="emitBrowsePageDelay">
|
||||
/// False omits <c>browsePageDelayMs</c> from the saved config (it was never set and the operator left the
|
||||
/// default), so a later change of the factory default is not pinned by an unrelated AdminUI save.
|
||||
/// </param>
|
||||
internal static string Serialize(AbCipDriverOptions options, string? originalJson = null, bool emitBrowsePageDelay = true)
|
||||
{
|
||||
var json = DriverConfigJsonOverlay.Apply(originalJson, JsonSerializer.Serialize(options, _jsonOpts), OwnedPaths, TimeSpanToMs);
|
||||
if (emitBrowsePageDelay || System.Text.Json.Nodes.JsonNode.Parse(json) is not System.Text.Json.Nodes.JsonObject obj) return json;
|
||||
var key = obj.Select(kv => kv.Key).FirstOrDefault(k => string.Equals(k, "browsePageDelayMs", StringComparison.OrdinalIgnoreCase));
|
||||
if (key is not null) obj.Remove(key);
|
||||
return obj.ToJsonString();
|
||||
}
|
||||
|
||||
private async Task EmitAsync()
|
||||
{
|
||||
// An invalid value is shown inline and NOT emitted: the saved config must never differ silently from
|
||||
// what the field displays (ToOptions would clamp a negative to 0).
|
||||
if (_form.BrowsePageDelayError is not null) return;
|
||||
var json = GetConfigJson();
|
||||
_lastParsed = json;
|
||||
_originalJson = json;
|
||||
@@ -221,7 +240,16 @@
|
||||
public bool EnableControllerBrowse { get; set; } = false;
|
||||
public bool EnableDeclarationOnlyUdtGrouping { get; set; } = false;
|
||||
// Milliseconds between controller symbol-browse pages (factory key browsePageDelayMs; default 5).
|
||||
public int BrowsePageDelayMs { get; set; } = 5;
|
||||
public const int DefaultBrowsePageDelayMs = 5;
|
||||
public int BrowsePageDelayMs { get; set; } = DefaultBrowsePageDelayMs;
|
||||
// True when the stored config carried the key; with the default value and no key, it stays absent.
|
||||
private bool _browsePageDelayLoaded;
|
||||
|
||||
/// <summary>Whether the save should write <c>browsePageDelayMs</c>: it was loaded, or differs from the default.</summary>
|
||||
public bool EmitsBrowsePageDelay => _browsePageDelayLoaded || BrowsePageDelayMs != DefaultBrowsePageDelayMs;
|
||||
|
||||
/// <summary>A validation message for the browse page delay, or null when valid.</summary>
|
||||
public string? BrowsePageDelayError => BrowsePageDelayMs < 0 ? "Browse page delay must be 0 or greater." : null;
|
||||
|
||||
// Alarm projection
|
||||
public bool EnableAlarmProjection { get; set; } = false;
|
||||
@@ -248,7 +276,9 @@
|
||||
f._loadedAlarmPoll = Ms(json, "alarmPollIntervalMs") ?? f._loadedAlarmPoll;
|
||||
f._loadedProbeInterval = Ms(json, "probe.intervalMs") ?? f._loadedProbeInterval;
|
||||
f._loadedProbeTimeout = Ms(json, "probe.timeoutMs") ?? f._loadedProbeTimeout;
|
||||
f.BrowsePageDelayMs = DriverConfigMs.Read(json, "browsePageDelayMs") ?? f.BrowsePageDelayMs;
|
||||
var browseDelay = DriverConfigMs.Read(json, "browsePageDelayMs");
|
||||
f._browsePageDelayLoaded = browseDelay.HasValue;
|
||||
f.BrowsePageDelayMs = browseDelay ?? f.BrowsePageDelayMs;
|
||||
f.TimeoutSeconds = DriverConfigMs.ToSeconds(f._loadedTimeout ?? o.Timeout);
|
||||
f.AlarmPollIntervalSeconds = DriverConfigMs.ToSeconds(f._loadedAlarmPoll ?? o.AlarmPollInterval);
|
||||
f.ProbeIntervalSeconds = DriverConfigMs.ToSeconds(f._loadedProbeInterval ?? o.Probe.Interval);
|
||||
|
||||
Reference in New Issue
Block a user