docs(raw): close the #468 follow-up — raw modals now feed pickers live merged config

Records the outcome in the design doc §6 (including the stale-open race the
review found and fixed in both modals), closes the follow-up in the tracking
doc and the universal-browser plan, and registers RawBrowseModal's shared
race shape in deferment.md §0.1 as the deliberate leftover.
This commit is contained in:
Joseph Doherty
2026-08-05 03:45:30 -04:00
parent 2c19a50bf9
commit c1e8762cd4
5 changed files with 102 additions and 20 deletions
@@ -1154,11 +1154,14 @@ neither agreed with a controller. Fixed, with the layout now pinned by a capture
### Follow-ups discovered during the first gate
1. ~~**Full tree-render verification is fixture-blocked.**~~ **CLOSED** by the second gate above.
2. **Driver-page address pickers pass no live config.** `AbCipDriverPage`/`TwinCATDriverPage`/`FOCASDriverPage`
host the same `*AddressPickerBody` but do **not** pass `DriverType`/`GetConfigJson`, so their (now-visible)
Browse button would capture against `"{}"` (no device). Out of Task 14's scope (which listed only the
TagEditor pass-through); wire the live form config through there too, or hide the Browse affordance on the
driver-config picker.
2. ~~**Driver-page address pickers pass no live config.**~~ — **CLOSED 2026-08-05.** The named
`*DriverPage` shells were deleted by v3 Batch 2 before this was ever actioned, but the defect
pattern survived into the v3 tag-editor hosts (`RawTagModal` / `RawManualTagEntryModal`, both
passing a `"{}"` stub as `GetDriverConfigJson`). Fixed by resolving the merged Driver+Device
config (`IRawTreeService.LoadMergedProbeConfigAsync` — the same blob Test-connect and
`RawBrowseModal` dial) once per open and forwarding a live accessor; fail-soft on resolve
failure. See `2026-08-05-raw-modal-picker-live-config-design.md` for the design and outcome
(including the stale-open race the review found and closed).
## Out of scope (explicitly)
@@ -104,10 +104,16 @@ marginal cost.
3. AbCip Test Connect misclassified the no-authored-tags probe **on real hardware too** — the
reachable-exception matcher looked for native C status spellings libplctag.NET never emits
(plus a sim-side 0x03/0x04 CIP status mismatch).
- **Residual follow-up (open):** the retired-flow driver pages (`AbCipDriverPage`/`TwinCATDriverPage`/
`FOCASDriverPage`) host the picker body without passing `DriverType`/`GetConfigJson`, so their
Browse button captures against `"{}"` — wire the live form config through or hide the affordance
there. Recorded in the implementation plan's first-gate follow-ups.
- **Residual follow-up — CLOSED 2026-08-05.** The named `*DriverPage` shells were already deleted
when v3 Batch 2 retired the routed `/clusters/{id}/drivers` flow, but the defect pattern had
survived into the v3 hosts: `RawTagModal` and `RawManualTagEntryModal` passed a `"{}"` stub as
`GetDriverConfigJson`, so the AbCip/TwinCAT/FOCAS pickers' "Browse controller" captured against
no endpoint. Fixed by resolving `LoadMergedProbeConfigAsync` per open and forwarding a live
accessor (design + outcome: `2026-08-05-raw-modal-picker-live-config-design.md`). Review of the
fix surfaced and closed a further stale-open race (a superseded open could install another
device's config); `RawBrowseModal` shares that pre-existing race shape and is recorded in
`deferment.md` §0.1, not fixed here. Pinned by `RawTagModalMergedConfigTests` +
`RawManualTagEntryModalMergedConfigTests`.
---
@@ -1,7 +1,8 @@
# Raw-modal address pickers: pass live merged config to browse — design
**Status:** design, 2026-08-05. Closes the last dangling follow-up from the #468 universal-browser
live gate ("driver-page address pickers pass no live config").
**Status:** IMPLEMENTED 2026-08-05 (unit suite green, AdminUI 1064/1064; live `/run` gate §4.2
pending). Closes the last dangling follow-up from the #468 universal-browser live gate
("driver-page address pickers pass no live config"). Outcome notes in §6.
## 1. Problem
@@ -50,7 +51,9 @@ Per modal, identically:
resolve must be prefetched, not lazy.
3. **Fail soft.** A null resolve (device gone, service error) leaves the stub in place — the picker
degrades to today's behaviour (graceful error chip on click), and the modal itself never blocks
or errors on the resolve. Wrap the call in try/catch; log at debug.
or errors on the resolve. Wrap the call in a swallowing try/catch carrying a constraint comment
(as built — neither modal injects a logger, matching the sibling `RawBrowseModal` pattern; the
picker reports its own error on use).
No changes to the picker bodies, the editors, `IRawTreeService`, or the browse services — the seam
already exists end-to-end; only the two stubs change.
@@ -88,3 +91,32 @@ bullet in `docs/plans/2026-07-24-driver-expansion-tracking.md` (Wave 0 section)
"Follow-ups discovered during the first gate" item 2 in
`docs/plans/2026-07-15-universal-discovery-browser-implementation.md` — both should note the pages
were already deleted and the surviving instance (the raw modals' stubs) is fixed.
## 6. Outcome (2026-08-05)
Shipped across five commits (`e698aee4` fake, `abb9f8a1` + `4e3a6e84` RawTagModal, `a2093b44` +
`c1c21f3f` + `2c19a50b` RawManualTagEntryModal), each TDD'd with observed-red evidence, and each
through a spec review + code review. Things the reviews changed or decided:
- **The review found a real race this design missed, and it was fixed in both modals.**
`OnParametersSetAsync` runs overlapped across rapid open/reopen (Blazor does not serialize it
against its own outstanding task), and nothing re-checked staleness after the awaits — so a
superseded open's late resolve could install **another device's** merged config while a different
tag/grid was displayed, and Browse would silently dial the wrong controller. Fixed with a
post-await staleness re-check before every write: `RawTagModal` re-checks its existing
`_loadedKey` (after BOTH awaits — the dto write had the same shape, previously masked only by the
EF concurrency token downstream), `RawManualTagEntryModal` a new per-open `_openGeneration`
counter (its `_open` bool cannot tell two opens apart). Both pinned by
`A_stale_open_never_feeds_the_picker_another_devices_config` facts, each observed red pre-guard;
`FakeRawTreeService` gained optional Task-returning delegates so tests can express overlap.
- ⚠️ **`RawBrowseModal` shares the same pre-existing race shape and was deliberately NOT touched**
(out of this branch's scope). Recorded in `deferment.md` §0.1. Lower stakes there — the browse
modal resolves the device it was opened for and shows its own error banner — but the shape is
identical.
- **Component-class tests must not set `[Parameter]` properties directly**: analyzer BL0005 is an
error under `TreatWarningsAsErrors`. The two test files legitimately solve it two ways (file-local
pragma vs a reflection `SetParameters` helper); the reflection helper is the preferred convention
going forward.
- The liveness of the forwarded `Func` is load-bearing (the pickers call it at click time, after
the async resolve) and is pinned in both files by retained-delegate facts, each falsified against
a deliberate capture-by-value implementation before being trusted.
@@ -1,12 +1,52 @@
{
"planPath": "docs/plans/2026-08-05-raw-modal-picker-live-config.md",
"tasks": [
{"id": 0, "subject": "Task 0: FakeRawTreeService test helper", "status": "pending"},
{"id": 1, "subject": "Task 1: RawTagModal — resolve and forward merged config", "status": "pending", "blockedBy": [0]},
{"id": 2, "subject": "Task 2: RawManualTagEntryModal — resolve and forward merged config", "status": "pending", "blockedBy": [0]},
{"id": 3, "subject": "Task 3: Full-suite verification", "status": "pending", "blockedBy": [1, 2]},
{"id": 4, "subject": "Task 4: Documentation bookkeeping", "status": "pending", "blockedBy": [3]},
{"id": 5, "subject": "Task 5: Live /run gate (main session)", "status": "pending", "blockedBy": [4]}
{
"id": 0,
"subject": "Task 0: FakeRawTreeService test helper",
"status": "completed"
},
{
"id": 1,
"subject": "Task 1: RawTagModal \u2014 resolve and forward merged config",
"status": "completed",
"blockedBy": [
0
]
},
{
"id": 2,
"subject": "Task 2: RawManualTagEntryModal \u2014 resolve and forward merged config",
"status": "completed",
"blockedBy": [
0
]
},
{
"id": 3,
"subject": "Task 3: Full-suite verification",
"status": "completed",
"blockedBy": [
1,
2
]
},
{
"id": 4,
"subject": "Task 4: Documentation bookkeeping",
"status": "completed",
"blockedBy": [
3
]
},
{
"id": 5,
"subject": "Task 5: Live /run gate (main session)",
"status": "pending",
"blockedBy": [
4
]
}
],
"lastUpdated": "2026-08-05T00:00:00Z"
}
"lastUpdated": "2026-08-05T18:00:00Z"
}