Files
lmxopcua/docs/plans/2026-08-05-raw-modal-picker-live-config-design.md
T
Joseph Doherty 3b9009da47 docs(drivers): tracker catches up with the #468 gate closure; design for the last dangling follow-up
The tracking doc and program-design §9 still showed Wave 0's live gate open —
it closed 2026-08-01 (c2c3c522). Both now record Waves 0-2 complete.

New design doc for the one residual item: the raw modals' typed tag editors
pass a '{}' stub as GetDriverConfigJson, so the AbCip/TwinCAT/FOCAS pickers'
Browse button captures against no endpoint and can never succeed. Decision:
wire the merged Driver+Device config through (the RawBrowseModal/Test-connect
seam already exists), not hide the button.
2026-08-05 03:14:31 -04:00

91 lines
5.3 KiB
Markdown

# 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").
## 1. Problem
The #468 first-gate follow-up named `AbCipDriverPage`/`TwinCATDriverPage`/`FOCASDriverPage` as
hosting `*AddressPickerBody` without passing `DriverType`/`GetConfigJson`. Those page shells were
deleted when v3 Batch 2 retired the routed `/clusters/{id}/drivers` flow — but the defect pattern
survived the migration into the two v3 hosts of the typed tag editors:
- `Components/Shared/Raw/RawTagModal.razor` (edit an existing raw tag) — `BuildEditorParameters()`
passes `["GetDriverConfigJson"] = (Func<string>)(() => "{}")`.
- `Components/Shared/Raw/RawManualTagEntryModal.razor` (manual multi-row tag entry) — same stub,
per row.
The editors (`AbCipTagConfigEditor`, `TwinCATTagConfigEditor`, `FocasTagConfigEditor`) forward that
`Func` to their `*AddressPickerBody`, whose browse gate is
`BrowserService.CanBrowse(DriverType, GetConfigJson())`. `DiscoveryDriverBrowser.CanBrowse` creates
a throwaway driver from the JSON — and `"{}"` parses fine into default options for AbCip / TwinCAT /
FOCAS, all `ITagDiscovery { SupportsOnlineDiscovery: true }`. So the **"Browse controller" button
renders enabled, and every click captures against an empty config** (no endpoint) — a guaranteed
failure chip. The affordance is a lie in exactly the hosts where an operator would re-point a tag.
## 2. Decision — wire it through (not hide)
The #468 note offered "wire the live form config through, or hide the Browse affordance". Wire it
through:
- Both hosts already hold the device context. `RawManualTagEntryModal` takes `DeviceId` as a
parameter; `RawTagModal` receives `RawTagEditDto.DeviceId` from `LoadTagForEditAsync`.
- The resolver already exists and is proven: `IRawTreeService.LoadMergedProbeConfigAsync(deviceId)`
→ `(DriverType, MergedConfigJson)?` — the **same Driver+Device merge** the Test-connect probe and
`RawBrowseModal` dial (endpoint lives in `DeviceConfig` in v3, so the merged blob is the only
JSON a browse capture can succeed against).
- Hiding would delete a legitimate workflow (browse while editing/creating a tag) whose whole
supporting machinery shipped with Wave 0.
## 3. Design
Per modal, identically:
1. **Resolve on open.** When the modal becomes visible with a device in hand (RawTagModal: after
`LoadTagForEditAsync` returns the dto; RawManualTagEntryModal: on open, from the `DeviceId`
parameter), call `LoadMergedProbeConfigAsync(deviceId)` and cache the result in a nullable
field `_mergedConfigJson`. Reset the cache when the modal closes/reopens for a different target.
2. **Forward it.** `["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}")`.
The `Func` is synchronous by contract (the picker body calls it inline), which is why the
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.
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.
### Out of scope
- The MQTT/Sparkplug and OpcUaClient pickers (bespoke browsers with their own config paths — not
hosted through these two stubs' editors, or already receiving config).
- Making the resolve lazy/async in the picker body (`Func<string>` contract change) — YAGNI.
- The two documented MQTT browse-UI gaps (tracked in `docs/drivers/Mqtt.md`).
## 4. Testing
The three MQTT live gates proved this defect class (hand-maintained AdminUI wiring left behind by a
driver-side feature) is invisible to green unit suites — so test at two layers:
1. **Unit (falsifiable against today's code).** The AdminUI csproj has
`InternalsVisibleTo("ZB.MOM.WW.OtOpcUa.AdminUI.Tests")` and precedent for testing razor
component classes directly (`RawDriverTypeDialogParityTests`). Make each modal's editor-parameter
construction internal-testable and assert the `GetDriverConfigJson` entry returns the cached
merged JSON when one is loaded — this test **fails against the current stub**. Also assert the
null-resolve fallback returns `"{}"`.
2. **Live `/run` gate (docker-dev).** Against the patched AbCip fixture (`10.100.0.35:44818`, the
#468 second-gate `ab_server` that serves `@tags`):
- Open the **edit modal** for an existing AbCip raw tag → "Browse controller" → the nine-tag
tree renders (was: error chip).
- Open **manual entry** for the same device → same result on a row's picker.
- Negative: a device whose endpoint is unreachable still shows the graceful error chip, and the
modal itself stays usable.
## 5. Bookkeeping
On completion, close the loop in the two places that record the follow-up: the "Residual follow-up"
bullet in `docs/plans/2026-07-24-driver-expansion-tracking.md` (Wave 0 section) and the
"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.