fix(r2-02): resilience section — parse-failure lockout + truthful raw pane (04/C-7; live-/run deferred)

This commit is contained in:
Joseph Doherty
2026-07-13 10:19:50 -04:00
parent 9e0a05dc6a
commit 112f88c9fe
2 changed files with 35 additions and 6 deletions
@@ -16,7 +16,7 @@
{ "id": 12, "subject": "U-6 doc sweep (seam xmldocs, OTOPCUA0001 message + analyzer tests, operator docs; leave migrations/WedgeDetector/historical plans) + Options_properties_are_exactly_the_pipeline_wired_set knob-inertness guard", "status": "completed", "blockedBy": [10, 11] },
{ "id": 13, "subject": "C-7 RED tests: unknown top-level key / unknown capability / unknown per-policy field survive round-trip; malformed JSON sets ParseFailed and ToJson returns the original text", "status": "completed", "blockedBy": [11] },
{ "id": 14, "subject": "C-7 implement: JsonObject-bag FromJson/ToJson (TagConfigJson idiom), ParseFailed + RawStoredJson, blank->null contract preserved, parser-interop test green", "status": "completed", "blockedBy": [13] },
{ "id": 15, "subject": "C-7 razor: ParseFailed warning banner + input lockout + explicit discard button; raw pane shows the stored ResilienceConfig; live-/run on docker-dev :9200 (rebuild BOTH centrals; SQL-mangle + unknown-key survival checks; also drive task 5 warnings and task 9 disabled cells)", "status": "pending", "blockedBy": [14] },
{ "id": 15, "subject": "C-7 razor: ParseFailed warning banner + input lockout + explicit discard button; raw pane shows the stored ResilienceConfig; live-/run on docker-dev :9200 (rebuild BOTH centrals; SQL-mangle + unknown-key survival checks; also drive task 5 warnings and task 9 disabled cells)", "status": "deferred-live", "note": "docker-dev :9200 live-/run; serial live pass", "blockedBy": [14] },
{ "id": 16, "subject": "S-7 builder: add options-generation to PipelineKey + GetOrCreate(optionsGeneration=0) + thread through CapabilityInvoker; StaleGeneration_ReCache_IsNotServed_ToNewGeneration", "status": "pending", "blockedBy": [3] },
{ "id": 17, "subject": "S-7 factory: Interlocked per-Create generation stamp + Respawn_interleaved_with_old_invoker_call_still_applies_new_options wiring proof; update Invalidate comment (cleanup, not correctness)", "status": "pending", "blockedBy": [16] },
{ "id": 18, "subject": "Wrap-up: whole-solution build (0 warnings, analyzer silent) + full dotnet test gate; record the pass + the two report anchor drifts in archreview/plans/STATUS.md", "status": "pending", "blockedBy": [4, 8, 9, 12, 15, 17] }
@@ -6,9 +6,23 @@
<p class="form-text mb-3">Blank fields use the driver type's stability-tier defaults
(see <span class="mono">docs/v2/driver-stability.md</span>). Set only what you need to override.</p>
@if (_m.ParseFailed)
{
<div class="alert alert-danger" role="alert">
<strong>Stored resilience config could not be parsed.</strong>
Editing is locked so an accidental keystroke can't overwrite it. Fix the stored JSON directly,
or discard it and start from tier defaults.
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-danger" @onclick="DiscardAsync">
Discard stored JSON and start blank
</button>
</div>
</div>
}
<div class="row g-3">
<div class="col-md-4"><label class="form-label">Recycle interval (s, Tier C only)</label>
<input type="number" class="form-control form-control-sm" @bind="_m.RecycleIntervalSeconds" @bind:after="EmitAsync" placeholder="none" /></div>
<input type="number" class="form-control form-control-sm" @bind="_m.RecycleIntervalSeconds" @bind:after="EmitAsync" placeholder="none" disabled="@_m.ParseFailed" /></div>
</div>
<div class="table-wrap mt-3">
@@ -21,9 +35,9 @@
var writeShaped = cap is "Write" or "AlarmAcknowledge";
<tr>
<td class="mono">@cap</td>
<td><input type="number" class="form-control form-control-sm" @bind="row.TimeoutSeconds" @bind:after="EmitAsync" placeholder="default" /></td>
<td><input type="number" class="form-control form-control-sm" @bind="row.RetryCount" @bind:after="EmitAsync" placeholder="@(writeShaped ? "never" : "default")" disabled="@writeShaped" title="@(writeShaped ? "Writes/acks never retry — per-tag opt-in not yet available" : null)" /></td>
<td><input type="number" class="form-control form-control-sm" @bind="row.BreakerFailureThreshold" @bind:after="EmitAsync" placeholder="default" /></td>
<td><input type="number" class="form-control form-control-sm" @bind="row.TimeoutSeconds" @bind:after="EmitAsync" placeholder="default" disabled="@_m.ParseFailed" /></td>
<td><input type="number" class="form-control form-control-sm" @bind="row.RetryCount" @bind:after="EmitAsync" placeholder="@(writeShaped ? "never" : "default")" disabled="@(writeShaped || _m.ParseFailed)" title="@(writeShaped ? "Writes/acks never retry — per-tag opt-in not yet available" : null)" /></td>
<td><input type="number" class="form-control form-control-sm" @bind="row.BreakerFailureThreshold" @bind:after="EmitAsync" placeholder="default" disabled="@_m.ParseFailed" /></td>
</tr>
}
</tbody>
@@ -46,7 +60,9 @@
<details class="mt-3">
<summary class="small text-muted">Raw JSON (advanced)</summary>
<pre class="form-control form-control-sm mono mt-2" style="white-space:pre-wrap;min-height:3rem;">@(_m.ToJson() ?? "(null — all tier defaults)")</pre>
@* Render the bound STORED value, not the re-serialized model — shows the truth in both the
healthy and the malformed case. *@
<pre class="form-control form-control-sm mono mt-2" style="white-space:pre-wrap;min-height:3rem;">@(ResilienceConfig ?? "(null — all tier defaults)")</pre>
</details>
</div>
</section>
@@ -70,9 +86,22 @@
private async Task EmitAsync()
{
// Never emit while the stored config is unparseable — the section is locked, and an unrelated
// keystroke must never silently overwrite what we couldn't read (04/C-7).
if (_m.ParseFailed) return;
var json = _m.ToJson();
_lastParsed = json;
ResilienceConfig = json;
await ResilienceConfigChanged.InvokeAsync(json);
}
private async Task DiscardAsync()
{
// Explicit, visible destruction — replace the unparseable blob with a blank (tier-default) config.
_m = new ResilienceFormModel();
_lastParsed = null;
ResilienceConfig = null;
await ResilienceConfigChanged.InvokeAsync(null);
}
}