docs(v3): Batch 2 — /raw authoring tree guide + CLAUDE.md + PR description
docs/Raw.md (the /raw project tree: lazy tree, endpoint→DeviceConfig split, manual/CSV/browse authoring, Calculation driver, routed-flow retirement); CLAUDE.md AdminUI section notes /raw + DriverTypeNames rewire + Calculation driver; PR body with the 7-item live-gate evidence. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
# The Raw project tree (`/raw`)
|
||||
|
||||
v3 authors device I/O in a **Raw project tree** — a cluster-rooted, Kepware-style hierarchy
|
||||
that is the single surface for driver/device/tag authoring. It is the peer of the `/uns`
|
||||
unified-namespace page; the two OPC UA namespaces (`ns=Raw` / `ns=UNS`) light up in Batch 4.
|
||||
|
||||
```
|
||||
Enterprise → Cluster → Folder(s) → Driver → Device → TagGroup(s) → Tag
|
||||
```
|
||||
|
||||
Every node's identity is its **RawPath** (`<Folder/…>/<Driver>/<Device>/<TagGroup/…>/<Tag>`,
|
||||
cluster-scoped, `/`-separated, ordinal). Names are validated as RawPath segments (no `/`, no
|
||||
leading/trailing whitespace) at authoring and at the deploy gate.
|
||||
|
||||
## The tree
|
||||
|
||||
`/raw` (`GlobalRaw.razor` → `RawTree.razor`, backed by `IRawTreeService`) lazily loads each
|
||||
container level on expand (unlike `/uns`, which eager-loads). Each node has a right-click
|
||||
context menu **and** a `⋯` fallback (the reusable `ContextMenu` component):
|
||||
|
||||
| Node | Actions |
|
||||
|---|---|
|
||||
| Cluster | New folder · New driver |
|
||||
| Folder | New folder · New driver · Rename · Delete |
|
||||
| Driver | Configure · New device · Enable/Disable · Rename · Delete |
|
||||
| Device | Configure (endpoint + **Test connect**) · New tag-group · Add tags ▸ · Delete |
|
||||
| TagGroup | New group · Add tags ▸ · Rename · Delete |
|
||||
| Tag | Edit · Delete |
|
||||
|
||||
Deletes are blocked while a node has children (or, for a raw tag, while a `UnsTagReference`
|
||||
points at it — the error names the referencing equipment). Renames return non-blocking
|
||||
**warnings**: renaming a node changes the RawPath of every historized / UNS-referenced tag
|
||||
beneath it (its historian tagname / UNS projection path moves).
|
||||
|
||||
## Endpoint → DeviceConfig (the channel/device split)
|
||||
|
||||
v3 moves the **connection endpoint** off the driver and onto the **Device** (`DeviceConfig`),
|
||||
mirroring Kepware's channel/device split. The driver's `DriverConfig` holds protocol/channel
|
||||
settings; the device's `DeviceConfig` holds host/port/endpoint. At deploy/probe time
|
||||
`DriverDeviceConfigMerger` merges the device's endpoint up into the driver config, and Test
|
||||
connect (inside the **Device** modal) probes that merged config. Driver forms no longer
|
||||
serialize the endpoint keys, so `DeviceConfig` is the single source of truth.
|
||||
|
||||
## Authoring tags
|
||||
|
||||
**Add tags ▸** on a Device or TagGroup offers:
|
||||
|
||||
- **Manual entry** — a grid of Name / DataType / AccessLevel / WriteIdempotent / PollGroup +
|
||||
the driver-typed `TagConfig` editor per row.
|
||||
- **Import/Export CSV** — a staged flow (upload → parse → **review grid** with per-row
|
||||
verdicts → commit) over an RFC-4180 parser. Columns: the common set
|
||||
(`Name, TagGroupPath, DataType, AccessLevel, WriteIdempotent, PollGroup, IsHistorized,
|
||||
HistorianTagname, IsArray, ArrayLength, Alarm.*, TagConfigJson`) plus the per-driver typed
|
||||
columns (e.g. Modbus `Region, Address, ModbusDataType, ByteOrder, …`). `TagGroupPath`
|
||||
auto-creates nested groups; typed columns win over `TagConfigJson` on conflict (flagged in
|
||||
the review grid). **Import is all-or-nothing** — any invalid row blocks the whole commit.
|
||||
Export round-trips the same shape.
|
||||
- **Browse device…** — enabled per the two-tier resolution (bespoke `IDriverBrowser` →
|
||||
universal `DiscoveryDriverBrowser` when the driver's `ITagDiscovery.SupportsOnlineDiscovery`
|
||||
is true → grayed out otherwise). Multi-selected leaves become raw `Tag` rows under the
|
||||
target, with the browsed reference written into the driver-typed `TagConfig` **address
|
||||
field** (`nodeId` / `tagPath` / …), never an identity key. An opt-in "create matching
|
||||
tag-groups" toggle mirrors the browse folder nesting. The browser dials a real ephemeral
|
||||
driver against the **merged** Driver+Device config.
|
||||
|
||||
## The `Calculation` driver
|
||||
|
||||
Signal-level calculated tags are ordinary raw tags bound to a `Calculation` driver
|
||||
(`DriverTypeNames.Calculation`), computed by C# scripts that read other tags' live values via
|
||||
`ctx.GetTag("<RawPath>")`. See `docs/plans/2026-07-15-calculation-driver-mini-design.md` for
|
||||
the full design. Highlights:
|
||||
|
||||
- One auto-created default `Engine` device; per-tag `TagConfig` is
|
||||
`{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }`.
|
||||
- The host feeds dependency values via the new `IDependencyConsumer` capability + a
|
||||
`DependencyConsumerMuxAdapter` on the per-node dependency mux; calc-of-calc chains work
|
||||
because a calc output re-enters the mux. The driver rebuilds its tag/dependency table on
|
||||
every redeploy (`ReinitializeAsync`) and re-registers its refs after the delta applies.
|
||||
- **Deploy gates** (`DraftValidator`): `scriptId` existence, and a hard **cycle gate**
|
||||
(`DependencyGraph.DetectCycles` over calc→calc edges; cross-driver refs are terminal) that
|
||||
rejects an A→B→A oscillation loop naming the members. Compile is not hard-gated (VirtualTag
|
||||
parity); a runtime compile/throw/timeout lands as Bad quality + a `script-logs` entry.
|
||||
|
||||
## Retirement note
|
||||
|
||||
The routed `/clusters/{id}/drivers` driver-authoring flow (`DriverTypePicker`,
|
||||
`DriverEditRouter`, the 8 per-type `*DriverPage` shells, and the per-cluster Drivers list) is
|
||||
**retired** — authoring lives in `/raw`. The extracted driver/device **form bodies** live on
|
||||
inside the `/raw` modals. The `DriverType` dispatch maps (`TagConfigEditorMap`,
|
||||
`TagConfigValidator`, `EquipmentTagConfigInspector`) are keyed off the single-source-of-truth
|
||||
`DriverTypeNames` constants (fixing the historical `TwinCat`/`Focas` drift).
|
||||
@@ -0,0 +1,74 @@
|
||||
# v3 Batch 2 — `/raw` project-tree AdminUI + `Calculation` driver
|
||||
|
||||
Implements `docs/plans/2026-07-15-v3-batch2-raw-ui-calculation-plan.md` (Track 0 + WP1–WP8),
|
||||
executed via the `/v3-batch 2` coordinator: contracts-first fan-out, worktree-isolated Opus
|
||||
agents per wave, a code-reviewer pass + green build/test after each wave, then the
|
||||
non-negotiable 7-item live gate on docker-dev.
|
||||
|
||||
## What landed
|
||||
|
||||
- **Track 0** — reusable `ContextMenu` (right-click + `⋯`, keyboard/focus); RFC-4180
|
||||
`CsvParser`/`CsvWriter`; `DriverTypeNames` constants + a reflection-discovery guard;
|
||||
`CalculationEvaluator` core (VirtualTag-evaluator parity).
|
||||
- **Wave A** — `IRawTreeService`/`RawTreeService` (lazy tree + mutations, integrity guards,
|
||||
rename-warnings); `/raw` page + lazy `RawTree` with per-node context menus.
|
||||
- **Wave B** — driver→embeddable-form refactor + `DriverConfigModal`/`DeviceModal`
|
||||
(endpoint→`DeviceConfig`, Test-connect via merged config); manual tag entry + tag edit
|
||||
modal + `Calculation` tag editor; CSV import (staged review grid) / export; all wired into
|
||||
the tree (name/confirm/driver-type dialogs + post-mutation refresh).
|
||||
- **Wave C** — browse re-target (two-tier gate, merged-config feed, commit → raw tags with
|
||||
address-field mapping + folder mirroring); `Calculation` driver (`IDependencyConsumer` +
|
||||
mux adapter, timer + change triggers, `DraftValidator` scriptId + cycle gates);
|
||||
`DriverTypeNames` rewire + retirement of the routed driver flow.
|
||||
|
||||
**Address space stays dark** (values light up in Batch 4). Full solution builds 0 errors;
|
||||
`AdminUI.Tests` 638 passed / 3 skipped, `Runtime.Tests` 357 passed, driver + guard suites green.
|
||||
|
||||
## Reviewer findings fixed in-branch
|
||||
|
||||
- **Wave B H1** — driver forms still serialized the endpoint into `DriverConfig`; stripped
|
||||
the endpoint keys (Modbus/S7/OpcUaClient) so `DeviceConfig` is the sole source (a 2nd
|
||||
device on a single-Host driver no longer reverts to `127.0.0.1`).
|
||||
- **Wave C HIGH** — `CalculationDriver.ReinitializeAsync` ignored the new config, so calc
|
||||
tag/script edits were silently inert after a redeploy and the re-register loop was
|
||||
decorative. Now rebuilds the tag/dependency table on reinit and re-registers deps after
|
||||
the delta applies (`DeltaApplied` notification, race-free). New redeploy test proves it.
|
||||
- Wave-A/B/0 mediums (auto-expand, friendly create-race/delete failures, discovery-driven
|
||||
guard, CSV comment). Deferred (documented): pre-existing Modbus/S7 form `TimeSpan`↔`*Ms`
|
||||
DTO mismatch; CSV alarm-subfield/flag-precedence edge cases; OpcUaClient endpoint
|
||||
precedence; unused `LoadMergedProbeConfigAsync`; refresh collapses sibling expansion.
|
||||
|
||||
## Live `/run` gate (docker-dev `:9200`, both central nodes rebuilt)
|
||||
|
||||
1. **Authoring + Test-connect** — `/raw` → New Modbus driver `gate-modbus` (form body renders,
|
||||
`PLC family`/`MELSEC` enum dropdowns correct) → Device1 endpoint `10.100.0.35:5020` in
|
||||
`DeviceConfig` → **Test connect OK · 94 ms** against the live Modbus fixture; SQL confirms
|
||||
endpoint in `DeviceConfig` (`{"Host":"10.100.0.35","Port":5020,"UnitId":1}`).
|
||||
2. **Historian-tagname deploy block** — a historized tag with a 260-char effective tagname →
|
||||
`POST /api/deployments` **422** `[HistorianTagnameTooLong] tag 'TAG-gatelong' … is 260 chars
|
||||
(max 255)`; shortened override → **202 Accepted**.
|
||||
3. **CSV round-trip + bad-enum** — export produces the exact column dictionary
|
||||
(`…,Region,Address,ModbusDataType,ByteOrder,…`); importing 1 valid + 1 bad-enum row →
|
||||
review grid "2 rows, 1 valid, 1 invalid", row-2 error *"Column 'ModbusDataType':
|
||||
'NOTATYPE' is not a valid ModbusDataType (expected one of: Bool, Int16, UInt16, …)"*,
|
||||
**Commit disabled** (all-or-nothing; SQL confirms nothing committed); a valid-only import →
|
||||
Commit → `ImportedTag` lands with typed columns assembled into `TagConfig`.
|
||||
4. **Browse-commit** — OpcUaClient device → opc-plc (`opc.tcp://10.100.0.35:50000`): two-tier
|
||||
gate enabled browse, merged config connected, real tree walked (NetworkSet/…/OpcPlc);
|
||||
multi-selected 2 leaves with "create matching tag-groups" → raw tags land under the
|
||||
mirrored `Basic` group with DataType + the browse ref in the `nodeId` address field
|
||||
(`nsu=…;s=AlternatingBoolean`), not an identity key.
|
||||
5. **Calculation deploy + cycle gate** — a calc tag reading a Modbus RawPath deploys **202**;
|
||||
a 2-cycle (`calc1/Engine/A`↔`calc1/Engine/B`) → **422** `[CalculationDependencyCycle] …
|
||||
(members: calc1/Engine/A, calc1/Engine/B)`; clean redeploy 202, no crash loops (both
|
||||
central nodes stable; only a deploy-reject WRN in the log).
|
||||
6. **Rename warning** — renaming `gate-modbus`→`gate-modbus-renamed` fired the warning modal
|
||||
*"1 historized tag beneath this node will change its RawPath (historian tagname)…"*.
|
||||
7. **Retired routes** — `/clusters/{id}/drivers`, `…/new`, `…/new/{slug}`, `…/{id}` all
|
||||
return **404**; `/raw` + `/uns` still 200.
|
||||
|
||||
## Docs
|
||||
|
||||
`docs/Raw.md` (new — the `/raw` authoring tree + endpoint split + CSV + Calculation +
|
||||
retirement note); `CLAUDE.md` AdminUI section updated; the Calculation mini-design remains the
|
||||
driver's authority.
|
||||
Reference in New Issue
Block a user