# 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/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 `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` §§1–8 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.