- 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.
12 KiB
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 ofUnsTreeService): 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 — queryUnsTagReferencejoins). - 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(/rawroute, nav entry next to/uns) +Components/Shared/Raw/RawTree.razor— modeled onGlobalUns/UnsTree/UnsNode, but actually exercising the lazy plumbing (HasLazyChildren/loading flags exist inUnsNode.csand were never used; reuse the pattern or make aRawNodetwin). - 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 intoComponents/Shared/Drivers/Forms/<Driver>DriverForm.razor; what stays behind is the@pageroute +EditForm+ ClusterNav + DB load + navigation. NewDriverConfigModal.razorhosts a form body by driver type (dispatch via T0-3DriverTypeNames). Keep the old routed pages working during v3 (they're deleted only when/rawfully replaces them — end of this batch, see WP8). - Device modal (new): with Batch 1's endpoint→
DeviceConfigmove, the multi-device drivers' embedded device collection editors (CollectionEditor.razorusage inside the pages) become a per-Device config modal editingDeviceConfigJSON 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+ theIDriverProbepath (transient config, no persistence) — probes now need the merged Driver+Device config (Batch 1'sDriverDeviceConfigMerger). - 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 entryon Device/TagGroup: grid of name, datatype, access,WriteIdempotent, poll group + the driver-typedTagConfigeditor per row (reuseComponents/Shared/Uns/TagEditors/*viaTagConfigEditorMap— re-keyed toDriverTypeNamesin 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
EquipmentImportBatchstaged 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 (ModbusRegion, Address, ModbusDataType, ByteOrder, BitIndex, StringLength, Writable; S7Address, S7DataType, StringLength, Writable; AbCipTagPath, AbCipDataType, Writable; AbLegacyAddress, AbLegacyDataType, Writable; TwinCATSymbolPath, TwinCATDataType, Writable; FOCASAddress, FocasDataType; OpcUaClientNodeId; GalaxyAttributeRef; CalculationScriptId, ChangeTriggered, TimerIntervalMs). Enum columns accept member names case-insensitively.TagGroupPathauto-creates nested groups.TagConfigJsonis 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
CsvColumnMapderived from the<Driver>TagConfigModelscalar surfaces — put it next to the models inAdminUI/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.SupportsOnlineDiscoveryexist (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 whenSupportsOnlineDiscovery→ grayed out + tooltip).- Commit contract (v3): multi-selected leaves → raw
Tagrows under the target Device/TagGroup —Namefrom browse name,DataTypefromDriverAttributeInfo, the leaf's driver reference written into the driver-typedTagConfigaddress field (nodeId/attributeRef/ the protocol address — NOT any identity key). Opt-in "create matching tag-groups" toggle mirrors browse folders onto nestedTagGroups. - Config input: pass the merged Driver+Device config (
DriverDeviceConfigMerger) as the browser'sconfigJson(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 §§1–8 verbatim)
- Project
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/around the T0-4 evaluator:CalculationDriver : IDriver, ISubscribable, IReadable(notIWritable, notITagDiscovery); the newIDependencyConsumercapability interface inCore.Abstractions(exact shape in mini-design §3);DriverHostActorwiring (~30 lines): when a spawned driver implements it, spawn a mux-adapter child thatRegisterInterest(driver.DependencyRefs, Self)onDependencyMuxActorand forwardsDependencyValueChangedintoOnDependencyValue; 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 dormantTimerTriggerSchedulerrevived inside the driver. - Deploy gates in
DraftValidator:scriptIdexistence in the draft generation; Tarjan cycle gate over calc→calc edges via the dormantDependencyGraph(cross-driver refs are terminal nodes). Compile deliberately NOT hard-gated (VT parity). - Error semantics: Bad + last-known value once per Good→Bad transition +
ScriptLogEntryonscript-logs; recovery force-published; historized Bad =BadInternalError. - Registration:
DriverFactoryBootstrap.AddOtOpcUaDriverFactories+AddOtOpcUaDriverProbes(probe = config-parse only, never throws); auto-created defaultEnginedevice;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(fixesTwinCat→TwinCAT,Focas→FOCAS),AdminUI/Uns/TagEditors/TagConfigEditorMap.cs,TagConfigValidator.cs, andControlPlane/AdminOperations/EquipmentTagConfigInspector.cs. AddCalculationentries where applicable. - Once
/rawcovers authoring end-to-end (this wave), retire the routed/clusters/{id}/driversflow: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):
/raw: create Folder → Modbus driver (form body renders, enum fields correct) → Device with endpoint10.100.0.35:5020inDeviceConfig→ Test connect green.- 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). - 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.
- 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.
- Calculation: create a
Calculationdriver → 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 indocker compose logs.) - Rename a driver → the UI warning seam fires for a historized tag beneath it.
- Confirm the retired
/clusters/{id}/driversroutes are gone from nav and 404/redirect cleanly.
Record each step's evidence in the PR.