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:
Joseph Doherty
2026-10-08 07:31:19 -04:00
parent ca0f1e7735
commit 1475991fd6
39 changed files with 478 additions and 48 deletions
@@ -328,4 +328,36 @@ public sealed class AbCipDriverPageFormSerializationTests
saved.ShouldContain("futureOption");
System.Text.Json.Nodes.JsonNode.Parse(saved)!.AsObject().ContainsKey("browsePageDelay").ShouldBeFalse();
}
// ---- review R3-m4: the key is emitted only when loaded or changed from the default; negatives are flagged ----
[Fact]
public void BrowsePageDelayMs_is_omitted_when_never_set_and_left_at_the_default()
{
var original = """{"futureOption":"keep-me","devices":[{"hostAddress":"ab://10.0.0.1/1,0"}]}""";
var loaded = AbCipDriverForm.TryDeserialize(original)!;
var form = AbCipDriverForm.FormModel.FromJson(original, loaded);
var saved = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), original, form.EmitsBrowsePageDelay);
System.Text.Json.Nodes.JsonNode.Parse(saved)!.AsObject().ContainsKey("browsePageDelayMs").ShouldBeFalse();
saved.ShouldContain("futureOption");
// Control: changing it away from the default DOES write it, and a loaded key is kept even at the default value.
form.BrowsePageDelayMs = 125;
var changed = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), original, form.EmitsBrowsePageDelay);
System.Text.Json.Nodes.JsonNode.Parse(changed)!["browsePageDelayMs"]!.GetValue<int>().ShouldBe(125);
var withKey = """{"browsePageDelayMs":5,"devices":[{"hostAddress":"ab://10.0.0.1/1,0"}]}""";
var f2 = AbCipDriverForm.FormModel.FromJson(withKey, AbCipDriverForm.TryDeserialize(withKey)!);
f2.EmitsBrowsePageDelay.ShouldBeTrue();
}
[Fact]
public void BrowsePageDelayMs_negative_is_a_validation_error_zero_is_valid()
{
var form = new AbCipDriverForm.FormModel { BrowsePageDelayMs = -1 };
form.BrowsePageDelayError.ShouldNotBeNull();
form.BrowsePageDelayMs = 0;
form.BrowsePageDelayError.ShouldBeNull();
}
}