cf03ca279d
- 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.
194 lines
12 KiB
Markdown
194 lines
12 KiB
Markdown
# 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` §§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.
|