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

12 KiB
Raw Blame History

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 TagGroups.
  • 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 TwinCatTwinCAT, FocasFOCAS), 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 DeviceConfigTest 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.