Files
lmxopcua/docs/plans/2026-07-15-v3-batch2-raw-ui-calculation-plan.md
T
Joseph Doherty cf03ca279d docs(v3): implementation plans — master orchestration + 4 batch plans
- v3 design finalized: all 7 open items resolved (live historian probe:
  255-char tagname limit; SDK multi-notifier spike: native AddNotifier,
  never duplicate ReportEvent; NamespaceKind.Simulated retired; CSV column
  dictionaries; ScadaBridge re-bind sized; rename-warning scan decided),
  reconciled with the pre-v3 universal discovery browser (its new §11
  v3 forward note re-targets the browse commit contract).
- Calculation pseudo-driver mini-design (IDependencyConsumer capability,
  mux-fed change/timer triggers, Tarjan cycle deploy gate, VT-parity
  error semantics).
- Implementation plans for Opus-agent execution, grounded against exact
  files/symbols: master plan (batch DAG, worktree-isolated parallel
  waves, contract-first fan-out, global gotcha list) + one plan per
  batch (1 schema+RawPath identity, 2 /raw UI+Calculation, 3 UNS
  reference-only+{{equip}}, 4 dual-namespace address space = v3.0),
  each with per-wave file ownership and a mandatory docker-dev live gate.
2026-07-15 16:50:30 -04:00

194 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v3 Batch 2 — `/raw` project-tree AdminUI + `Calculation` driver
**Branch:** `v3/batch2-raw-ui` (opened from master after Batch 1 merges)
**Design authority:** v3 design §"AdminUI — the Raw project tree (`/raw`)", §"Calculated tags", the CSV appendix; `2026-07-15-calculation-driver-mini-design.md` (complete spec for B2-WP7); `2026-07-15-universal-discovery-browser-design.md` §11 (B2-WP6 only).
**Inputs from Track 0:** T0-1 `ContextMenu`, T0-2 CSV parser, T0-3 `DriverTypeNames`, T0-4 Calculation evaluator core — merge them onto this branch first if they haven't reached master.
**End state:** everything in the Raw tree is authorable end-to-end on docker-dev (tree, drivers, devices, tag-groups, tags via manual/CSV/browse, calc tags), Test-connect works, deploys succeed. **No live values are observable — Batch 4.**
## Work packages
### Wave A — tree + data service (2 agents)
**B2-WP1 — `IRawTreeService` + lazy tree data layer**
- New `AdminUI/Uns/IRawTreeService.cs` + `RawTreeService.cs` (peer of
`UnsTreeService`): cluster-rooted queries returning children per level
(Folder/Driver/Device/TagGroup/Tag), **paged/lazy** (per-device tag counts can be
large), plus mutation methods (create/rename/delete per node type, move-into-folder)
enforcing the integrity rules with user-readable errors (delete-blocked messages name
the referencing equipment — query `UnsTagReference` joins).
- Rename methods are where the **rename warning** hooks (Batch 3 adds the script-scan;
this batch wires the seam: rename returns a warnings list the UI shows — historized /
UNS-referenced checks are already answerable from this batch's schema).
- Unit tests against the EF model (in-memory + the DB-backed compliance suite pattern).
**B2-WP2 — `/raw` page + tree component**
- New `Components/Pages/Raw/GlobalRaw.razor` (`/raw` route, nav entry next to `/uns`) +
`Components/Shared/Raw/RawTree.razor` — modeled on `GlobalUns`/`UnsTree`/`UnsNode`, but
**actually exercising the lazy plumbing** (`HasLazyChildren`/loading flags exist in
`UnsNode.cs` and were never used; reuse the pattern or make a `RawNode` twin).
- Context menus per node type via T0-1 `ContextMenu` (menu items exactly as the design's
list: Folder/Driver/Device/TagGroup/Tag), incl. the "⋯" fallback affordance.
- Modals are stubs in this wave (each menu action opens a placeholder) — Waves B/C fill
them. This keeps WP2 mergeable early so later packages build on a rendering tree.
- Live-check on docker-dev before merge (no bUnit — the tree, lazy expansion, and menu
must be seen working).
### Wave B — driver/device/tag authoring modals (3 agents, parallel)
**B2-WP3 — driver + device config modals (the page-to-modal refactor)**
- Refactor the 8 typed driver pages
(`Components/Pages/Clusters/Drivers/{Modbus,AbCip,AbLegacy,S7,TwinCAT,Focas,OpcUaClient,Galaxy}DriverPage.razor`)
into embeddable form bodies: extract each page's form content into
`Components/Shared/Drivers/Forms/<Driver>DriverForm.razor`; what stays behind is the
`@page` route + `EditForm` + ClusterNav + DB load + navigation. New
`DriverConfigModal.razor` hosts a form body by driver type (dispatch via T0-3
`DriverTypeNames`). Keep the old routed pages working during v3 (they're deleted only
when `/raw` fully replaces them — end of this batch, see WP8).
- **Device modal (new):** with Batch 1's endpoint→`DeviceConfig` move, the multi-device
drivers' embedded device collection editors (`CollectionEditor.razor` usage inside the
pages) become a per-Device config modal editing `DeviceConfig` JSON via typed
per-driver device forms. Single-endpoint drivers' default device gets the same modal
(endpoint field lives here now).
- Test-connect inside the modal reuses `DriverTestConnectButton` + the `IDriverProbe`
path (transient config, no persistence) — probes now need the **merged
Driver+Device config** (Batch 1's `DriverDeviceConfigMerger`).
- **Enum-serialization guard:** every form/probe serialization site uses
`JsonStringEnumConverter` (the systemic FB-9/FB-10 bug class); add a round-trip test
per driver form model.
**B2-WP4 — manual tag entry + tag edit modal**
- `Add tags ▸ Manual entry` on Device/TagGroup: grid of name, datatype, access,
`WriteIdempotent`, poll group + the driver-typed `TagConfig` editor per row (reuse
`Components/Shared/Uns/TagEditors/*` via `TagConfigEditorMap` — re-keyed to
`DriverTypeNames` in WP8). Single-tag Edit modal for the Tag node reuses the same
editor shell.
- Name validation inline (`RawPaths.Validate` — no `/`, no lead/trail whitespace) +
sibling-uniqueness error surfaced from the service.
- New editor registration for `Calculation` (see WP7): `CalculationTagConfigModel`
(`scriptId`, `changeTriggered`, `timerIntervalMs`) + editor razor reusing the
VirtualTagModal's script dropdown/"New script"/inline Monaco panel components.
**B2-WP5 — CSV import/export**
- Uses T0-2 parser. Staged flow (all new code — the old `EquipmentImportBatch` staged
tables were orphaned dead code and are already dropped): upload → parse → **review
grid** (per-row validation verdicts) → commit.
- Columns per the design appendix: common
`Name, TagGroupPath, DataType, AccessLevel, WriteIdempotent, PollGroup, IsHistorized,
HistorianTagname, IsArray, ArrayLength, Alarm.AlarmType, Alarm.Severity,
Alarm.HistorizeToAveva, TagConfigJson` + the per-driver typed columns table (Modbus
`Region, Address, ModbusDataType, ByteOrder, BitIndex, StringLength, Writable`; S7
`Address, S7DataType, StringLength, Writable`; AbCip `TagPath, AbCipDataType,
Writable`; AbLegacy `Address, AbLegacyDataType, Writable`; TwinCAT `SymbolPath,
TwinCATDataType, Writable`; FOCAS `Address, FocasDataType`; OpcUaClient `NodeId`;
Galaxy `AttributeRef`; Calculation `ScriptId, ChangeTriggered, TimerIntervalMs`).
Enum columns accept member names case-insensitively. `TagGroupPath` auto-creates
nested groups. `TagConfigJson` is the fallback for unmapped keys and **merges under**
typed columns (typed column wins on conflict — document in the review grid).
- Export produces the same shape (round-trip property test: export → import → identical
tag set).
- Mapping layer: per-driver `CsvColumnMap` derived from the `<Driver>TagConfigModel`
scalar surfaces — put it next to the models in `AdminUI/Uns/TagEditors/` so model and
map evolve together; a reflection test asserts every typed column matches a model
property.
### Wave C — browse + Calculation + canonicalization (3 agents, parallel)
**B2-WP6 — browse re-target (GATED: requires the universal `DiscoveryDriverBrowser` merged pre-v3)**
Precondition check first: `IDriverBrowser` two-tier resolution + `DiscoveryDriverBrowser`
+ `ITagDiscovery.SupportsOnlineDiscovery` exist (they do NOT as of 2026-07-15). If absent,
this package detaches to a follow-up PR and the batch gate skips its leg.
- `Browse device…` on Device/TagGroup opens the browse modal reusing the session/tree
layer as-is (`IBrowseSession`, `BrowseSessionRegistry`, `BrowserSessionService`,
`DriverBrowseTree.razor`); enable/disable per the two-tier gate (bespoke browser →
universal when `SupportsOnlineDiscovery` → grayed out + tooltip).
- **Commit contract (v3):** multi-selected leaves → raw `Tag` rows under the target
Device/TagGroup — `Name` from browse name, `DataType` from `DriverAttributeInfo`, the
leaf's driver reference written into the driver-typed `TagConfig` **address field**
(`nodeId` / `attributeRef` / the protocol address — NOT any identity key). Opt-in
"create matching tag-groups" toggle mirrors browse folders onto nested `TagGroup`s.
- **Config input:** pass the merged Driver+Device config (`DriverDeviceConfigMerger`) as
the browser's `configJson` (the universal browser dials a real ephemeral driver
instance and needs the device endpoint).
- Name-collision note: the browser side must not reuse the name
`CapturingAddressSpaceBuilder` — Runtime owns it
(`Runtime/Drivers/CapturingAddressSpaceBuilder.cs`).
**B2-WP7 — `Calculation` driver (execute `2026-07-15-calculation-driver-mini-design.md` §§18 verbatim)**
- Project `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/` around the T0-4 evaluator:
`CalculationDriver : IDriver, ISubscribable, IReadable` (not `IWritable`, not
`ITagDiscovery`); the new **`IDependencyConsumer`** capability interface in
`Core.Abstractions` (exact shape in mini-design §3); `DriverHostActor` wiring (~30
lines): when a spawned driver implements it, spawn a mux-adapter child that
`RegisterInterest(driver.DependencyRefs, Self)` on `DependencyMuxActor` and forwards
`DependencyValueChanged` into `OnDependencyValue`; re-register on every apply.
**Register-AND-consume trap:** the DoD includes a Runtime test proving a calc driver's
deps actually flow (not just that the interface exists).
- Triggers: change-gate à la `VirtualTagActor` (no publish until all deps seen; dedupe) +
timer via the dormant `TimerTriggerScheduler` revived inside the driver.
- Deploy gates in `DraftValidator`: `scriptId` existence in the draft generation; Tarjan
cycle gate over calc→calc edges via the dormant `DependencyGraph` (cross-driver refs
are terminal nodes). Compile deliberately NOT hard-gated (VT parity).
- Error semantics: Bad + last-known value once per Good→Bad transition + `ScriptLogEntry`
on `script-logs`; recovery force-published; historized Bad = `BadInternalError`.
- Registration: `DriverFactoryBootstrap.AddOtOpcUaDriverFactories` +
`AddOtOpcUaDriverProbes` (probe = config-parse only, never throws); auto-created
default `Engine` device; `DriverTypeNames.Calculation`.
- Tests per mini-design §8 (unit + the 2-node harness integration case).
**B2-WP8 — `DriverTypeNames` rewire + old-page retirement**
- Re-key the four drifted dispatch maps to T0-3 constants:
`DriverEditRouter._componentMap` (fixes `TwinCat``TwinCAT`, `Focas``FOCAS`),
`AdminUI/Uns/TagEditors/TagConfigEditorMap.cs`, `TagConfigValidator.cs`, and
`ControlPlane/AdminOperations/EquipmentTagConfigInspector.cs`. Add `Calculation`
entries where applicable.
- Once `/raw` covers authoring end-to-end (this wave), retire the routed
`/clusters/{id}/drivers` flow: `DriverTypePicker`, `DriverEditRouter`, the 8 page
shells (their extracted form bodies live on in the modals). Nav + any deep links
updated.
## Wave schedule
| Wave | Packages | Agents | Notes |
|---|---|---|---|
| 0 | Merge Track 0 outputs onto the branch | — | T0-1..T0-4 |
| A | WP1 ∥ WP2 | 2 | Service vs components — disjoint; WP2 stubs modals |
| B | WP3 ∥ WP4 ∥ WP5 | 3 | Disjoint: driver forms vs tag editors/grid vs CSV. All three consume WP1/WP2 |
| C | WP6 ∥ WP7 ∥ WP8 | 3 | WP6 gated on the universal browser; WP7 is mostly Runtime/driver-side (disjoint from UI packages); WP8 last-merged in the wave (it deletes pages others may still reference — coordinator merges WP8 after WP3/WP6) |
## Batch verification gate (live `/run` on docker-dev — authoring + probe only)
Rebuild **both** central nodes, `docker compose up -d`, bring up fixtures
(`lmxopcua-fix up modbus standard`, `lmxopcua-fix up s7 s7_1500`). Then on
`http://localhost:9200` (login disabled):
1. `/raw`: create Folder → Modbus driver (form body renders, enum fields correct) →
Device with endpoint `10.100.0.35:5020` in `DeviceConfig`**Test connect green**.
2. TagGroup → Manual entry: add 3 tags incl. one historized with a >255-char effective
tagname → **deploy blocked by DraftValidator naming the tag**; shorten/override →
deploy succeeds (POST `/api/deployments`, `X-Api-Key`).
3. CSV: export the device's tags, delete one, re-import the file → review grid shows the
re-add, commit restores it. Import a file with a bad enum value → row-level error in
the grid, no partial commit.
4. Browse (if WP6 landed): browse an AbCip/OpcUaClient device, multi-select leaves with
"create matching tag-groups" → raw tags land under mirrored groups with correct
DataType + address field.
5. Calculation: create a `Calculation` driver → author a calc tag whose script reads the
Modbus tag's RawPath → deploy succeeds; author a 2-cycle (A reads B, B reads A) →
**deploy blocked naming the cycle members**. (Computed values are NOT visible yet —
Batch 4; the script-log page must stay silent, no crash loops in
`docker compose logs`.)
6. Rename a driver → the UI warning seam fires for a historized tag beneath it.
7. Confirm the retired `/clusters/{id}/drivers` routes are gone from nav and 404/redirect
cleanly.
Record each step's evidence in the PR.