diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs
new file mode 100644
index 00000000..8dd05184
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs
@@ -0,0 +1,104 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
+
+///
+/// Mutable, all-nullable form model for the driver resilience override. Binds the typed
+/// fields in DriverResilienceSection; null/blank = "use the driver's tier default", so a
+/// blank form serializes back to null (preserving DriverInstance.ResilienceConfig = null).
+/// Emits / reads the exact override JSON shape DriverResilienceOptionsParser consumes.
+///
+public sealed class ResilienceFormModel
+{
+ public static readonly string[] Capabilities =
+ ["Read", "Write", "Discover", "Subscribe", "Probe", "AlarmSubscribe", "AlarmAcknowledge", "HistoryRead"];
+
+ public int? BulkheadMaxConcurrent { get; set; }
+ public int? BulkheadMaxQueue { get; set; }
+ public int? RecycleIntervalSeconds { get; set; }
+
+ // capability name -> (timeout, retry, breaker), each nullable.
+ public Dictionary Policies { get; set; } =
+ Capabilities.ToDictionary(c => c, _ => new CapabilityRow());
+
+ public sealed class CapabilityRow
+ {
+ public int? TimeoutSeconds { get; set; }
+ public int? RetryCount { get; set; }
+ public int? BreakerFailureThreshold { get; set; }
+ public bool IsEmpty => TimeoutSeconds is null && RetryCount is null && BreakerFailureThreshold is null;
+ }
+
+ private static readonly JsonSerializerOptions ReadOpts = new() { PropertyNameCaseInsensitive = true };
+ private static readonly JsonSerializerOptions WriteOpts = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ public static ResilienceFormModel FromJson(string? json)
+ {
+ var model = new ResilienceFormModel();
+ if (string.IsNullOrWhiteSpace(json)) return model;
+
+ Shape? shape;
+ try { shape = JsonSerializer.Deserialize(json, ReadOpts); }
+ catch (JsonException) { return model; } // malformed -> empty form; raw view (next task) shows the text
+ if (shape is null) return model;
+
+ model.BulkheadMaxConcurrent = shape.BulkheadMaxConcurrent;
+ model.BulkheadMaxQueue = shape.BulkheadMaxQueue;
+ model.RecycleIntervalSeconds = shape.RecycleIntervalSeconds;
+ if (shape.CapabilityPolicies is not null)
+ foreach (var (cap, p) in shape.CapabilityPolicies)
+ if (model.Policies.TryGetValue(cap, out var row))
+ {
+ row.TimeoutSeconds = p.TimeoutSeconds;
+ row.RetryCount = p.RetryCount;
+ row.BreakerFailureThreshold = p.BreakerFailureThreshold;
+ }
+ return model;
+ }
+
+ /// Emit only the non-null overrides; returns null when nothing is overridden.
+ public string? ToJson()
+ {
+ var caps = Policies
+ .Where(kv => !kv.Value.IsEmpty)
+ .ToDictionary(kv => kv.Key, kv => new PolicyShape
+ {
+ TimeoutSeconds = kv.Value.TimeoutSeconds,
+ RetryCount = kv.Value.RetryCount,
+ BreakerFailureThreshold = kv.Value.BreakerFailureThreshold,
+ });
+
+ var hasAny = BulkheadMaxConcurrent is not null || BulkheadMaxQueue is not null
+ || RecycleIntervalSeconds is not null || caps.Count > 0;
+ if (!hasAny) return null;
+
+ var shape = new Shape
+ {
+ BulkheadMaxConcurrent = BulkheadMaxConcurrent,
+ BulkheadMaxQueue = BulkheadMaxQueue,
+ RecycleIntervalSeconds = RecycleIntervalSeconds,
+ CapabilityPolicies = caps.Count > 0 ? caps : null,
+ };
+ return JsonSerializer.Serialize(shape, WriteOpts);
+ }
+
+ private sealed class Shape
+ {
+ public int? BulkheadMaxConcurrent { get; set; }
+ public int? BulkheadMaxQueue { get; set; }
+ public int? RecycleIntervalSeconds { get; set; }
+ public Dictionary? CapabilityPolicies { get; set; }
+ }
+
+ private sealed class PolicyShape
+ {
+ public int? TimeoutSeconds { get; set; }
+ public int? RetryCount { get; set; }
+ public int? BreakerFailureThreshold { get; set; }
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ResilienceFormModelTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ResilienceFormModelTests.cs
new file mode 100644
index 00000000..12e8a65f
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ResilienceFormModelTests.cs
@@ -0,0 +1,41 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Resilience;
+
+public class ResilienceFormModelTests
+{
+ [Fact]
+ public void Blank_form_serializes_to_null()
+ => new ResilienceFormModel().ToJson().ShouldBeNull();
+
+ [Fact]
+ public void Partial_override_round_trips()
+ {
+ var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
+ m.Policies["Read"].TimeoutSeconds = 5;
+ m.Policies["Read"].RetryCount = 5;
+
+ var json = m.ToJson();
+ json.ShouldNotBeNull();
+
+ var back = ResilienceFormModel.FromJson(json);
+ back.BulkheadMaxConcurrent.ShouldBe(16);
+ back.Policies["Read"].TimeoutSeconds.ShouldBe(5);
+ back.Policies["Write"].IsEmpty.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Emitted_json_is_consumable_by_the_runtime_parser()
+ {
+ var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
+ m.Policies["Read"].TimeoutSeconds = 7;
+
+ var opts = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, m.ToJson(), out var diag);
+ diag.ShouldBeNull();
+ opts.BulkheadMaxConcurrent.ShouldBe(16);
+ opts.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(7);
+ opts.Resolve(DriverCapability.Write).RetryCount.ShouldBe(0);
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ZB.MOM.WW.OtOpcUa.AdminUI.Tests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ZB.MOM.WW.OtOpcUa.AdminUI.Tests.csproj
index 61ad438d..18546af9 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ZB.MOM.WW.OtOpcUa.AdminUI.Tests.csproj
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ZB.MOM.WW.OtOpcUa.AdminUI.Tests.csproj
@@ -19,6 +19,7 @@
+