DriverConfigModal, DeviceModal and RawTagModal built a new parameter dictionary
and new EventCallbacks every render. ParameterDictionaryCache reuses the
dictionary while the values it carries are unchanged and rebuilds it with the
current value when one changes (a build-once cache would strand edits); the
callbacks are created once. Blazor still re-sets an EventCallback-bearing
child's parameters per parent render, so the forms' _lastParsed guards stay.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The import review grid rendered every parsed row and re-scanned all rows three
times per render for its counts; RawCsvReviewView recomputes rows + counts once
per change and the grid renders through Virtualize. The export modal embedded the
whole CSV twice (textarea + base64 data: URI); it now shows a 50-line preview
and links to GET /api/raw/tags.csv (ConfigEditor, like /raw), which runs the same
RawTagCsvExport builder and sends it as an attachment.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The recursive RenderNode rendered every row of an expanded device, and built a
fresh context-menu list + closures per node per render. RawTreeRows flattens the
expanded tree (explicit stack) into rows rendered by one Virtualize; each level
shows 500 children, then a 'Show more' row. RawNodeMenuCache keys menus weakly
on the node instance and rebuilds on a picker-mode or Enabled change.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed.
m1: DisposeBridge cancels its CTS when the 5 s wait times out.
m2: a CSV import that commits after the modal closed still raises OnSaved.
m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md.
m4: browsePageDelayMs written only when loaded or changed; negative flagged.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Blazor does not serialize OnParametersSetAsync against its own outstanding
task, and the modal's `_open` bool cannot tell two opens apart. ResetAndResolveAsync
awaits LoadMergedProbeConfigAsync and ResolveGroupContextAsync and then wrote
_driverType/_mergedConfig/_effectiveDeviceId/_groupPrefix/_canBrowse/_disabledReason
with no staleness re-check, so a close-reopen onto another device let the first
open's late continuation install its device's browse context under the displayed
modal — "Connect & browse" would silently dial the wrong controller and the commit
would land under the wrong tag-group.
Ports the per-open `_openGeneration` counter proven in the two sibling modals
(RawManualTagEntryModal, RawTagModal, 2026-08-05) to every post-await write here:
after both awaits, in the catch's error banner, and in the finally. The finally is
the one departure from the sibling shape and it is load-bearing — this modal resolves
inside a try/finally, so a stale `return` still runs the tail and would drop the LIVE
open's spinner mid-resolve.
Pinned by four RawBrowseModalStaleOpenTests facts, each observed red before the guard
and each re-falsified afterwards by reverting only its own guard. The modal calls
StateHasChanged on the open path, which throws without a render handle, so the test
attaches an inert Renderer subclass (file-local BL0006 pragma); [Parameter]s are set
by reflection per the BL0005 convention.
AdminUI suite 1117/1117 green.
Task 14 — the P1 milestone gate. Ran the live /run verification on a docker-dev
rig against the real Mosquitto TLS+auth fixture, and recorded the result.
The gate did its job: it found MQTT was unauthorable in production.
RawDriverTypeDialog's driver-type list is a hand-maintained array that nobody
added MQTT to, so the /raw "New driver" picker never offered it — with every
other layer (contracts, driver, browser, factory, host registration, typed tag
editor) complete and 266 green unit tests. Nothing tied that array to
DriverTypeNames and AdminUI has no bUnit, so no offline test could see it.
Fixed, plus RawDriverTypeDialogParityTests: a reflection parity guard that
fails for ANY DriverTypeNames entry missing from the picker. Verified
falsifiable — RED with "…cannot be authored from the /raw New-driver picker,
so they are unreachable in production: Mqtt" before the one-line fix.
A second gap is left OPEN and documented, not fixed (no task ever covered it):
MqttDriverForm / MqttDeviceForm do not exist, and DriverConfigModal/DeviceModal
have no raw-JSON fallback — so an operator can create an MQTT driver but cannot
author its broker endpoint or credentials. P1 is not operator-complete until
those two razor forms land. The gate authored config via SQL to get past it.
Live gate result (isolated 2-node MAIN stack, image built from this branch,
fixture at 10.100.0.35:8883, AllowUntrustedServerCertificate rather than
pinning the CA into the rig):
- typed MQTT tag editor renders and round-trips; Json shows the JSON-path
field, Raw/Scalar hide it; wildcard topic a/+/c blocked at save; data-type
list offers Float64 and no Double; qos/retainSeed absent on "(driver
default)" and qos:2 written as a number; isHistorized/historianTagname
survive a topic-only edit; SparkplugB renders the P2 placeholder and
switching back keeps Plain values; jsonPath is guidance not a gate in all
three shapes ($ seeded only for a brand-new blank tag, blank stays absent,
clearing saves fine)
- deployed, and both editor-written blobs resolved and served changing live
values through OPC UA (22.8 / 1629, StatusCode Good) — no BadNodeIdUnknown
- the bespoke #-observation browser drove a real broker session and rendered
the fixture's actual topic tree (line1/{counter,state,temperature},
noise/chatter, retained/seed); a Modbus device's picker still correctly
reports "Browsing unavailable"
Also found live and documented (not a code change): both nodes of a redundant
pair run the driver, so a FIXED clientId makes them evict each other forever —
the broker logs "already connected, closing old connection" and both nodes
reconnect every ~2s while still reporting Healthy. Unset (the default) is
correct; confirmed 0 reconnects/60s on both nodes after removing it.
Suites: offline 1134 passed / 0 failed (266 Driver.Mqtt + 730 AdminUI incl. the
3 new guards + 138 Core.Abstractions); live 7/7 against the fixture.
Docs: new docs/drivers/Mqtt.md + Mqtt-Test-Fixture.md (the per-driver + fixture
convention every other driver follows), MQTT rows in docs/drivers/README.md,
TestConnectProbes.md, root README.md, and infra/README.md §3.
CLAUDE.md drift corrected while adding the MQTT endpoints:
- the "every fixture carries project: lmxopcua" claim was false — only the
MQTT fixture does; the filter returns one stack, not the fleet
- lmxopcua-fix.ps1 is Windows-VM-only; on macOS drive the host over ssh/rsync
(and the docker-dev rig itself runs locally under OrbStack)
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW