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.
5.3 KiB
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.
RawManualTagEntryModaltakesDeviceIdas a parameter;RawTagModalreceivesRawTagEditDto.DeviceIdfromLoadTagForEditAsync. - The resolver already exists and is proven:
IRawTreeService.LoadMergedProbeConfigAsync(deviceId)→(DriverType, MergedConfigJson)?— the same Driver+Device merge the Test-connect probe andRawBrowseModaldial (endpoint lives inDeviceConfigin 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:
- Resolve on open. When the modal becomes visible with a device in hand (RawTagModal: after
LoadTagForEditAsyncreturns the dto; RawManualTagEntryModal: on open, from theDeviceIdparameter), callLoadMergedProbeConfigAsync(deviceId)and cache the result in a nullable field_mergedConfigJson. Reset the cache when the modal closes/reopens for a different target. - Forward it.
["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}"). TheFuncis synchronous by contract (the picker body calls it inline), which is why the resolve must be prefetched, not lazy. - 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:
- 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 theGetDriverConfigJsonentry returns the cached merged JSON when one is loaded — this test fails against the current stub. Also assert the null-resolve fallback returns"{}". - Live
/rungate (docker-dev). Against the patched AbCip fixture (10.100.0.35:44818, the #468 second-gateab_serverthat 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.