diff --git a/docs/plans/2026-08-20-grouped-connection-bindings-design.md b/docs/plans/2026-08-20-grouped-connection-bindings-design.md index 23a3f068..57628481 100644 --- a/docs/plans/2026-08-20-grouped-connection-bindings-design.md +++ b/docs/plans/2026-08-20-grouped-connection-bindings-design.md @@ -1,6 +1,6 @@ # Grouped Connection Bindings — design -**Date:** 2026-08-20 · **Status:** approved, implementation pending · **Components:** Central UI (#15), Template Engine (flattening), CLI +**Date:** 2026-08-20 · **Status:** implemented 2026-08-20 on `feat/grouped-connection-bindings` (plan `2026-08-20-grouped-connection-bindings.md`, 9 tasks) · **Components:** Central UI (#15), Template Engine (flattening), CLI ## Problem diff --git a/docs/requirements/Component-CLI.md b/docs/requirements/Component-CLI.md index b7f89a89..f71a1499 100644 --- a/docs/requirements/Component-CLI.md +++ b/docs/requirements/Component-CLI.md @@ -84,6 +84,7 @@ scadabridge instance list [--site-id ] [--template-id ] [--search scadabridge instance get --id scadabridge instance create --name --template-id --site-id [--area-id ] scadabridge instance set-bindings --id --bindings +scadabridge instance bind-module --id --module --connection --object [--dry-run] scadabridge instance set-overrides --id --overrides scadabridge instance import-overrides --id --file scadabridge instance alarm-override set --instance-id --alarm [--trigger-config ] [--priority ] diff --git a/docs/requirements/Component-CentralUI.md b/docs/requirements/Component-CentralUI.md index f75b1008..a7b0a9f7 100644 --- a/docs/requirements/Component-CentralUI.md +++ b/docs/requirements/Component-CentralUI.md @@ -119,7 +119,11 @@ Central cluster only. Sites have no user interface. ### Instance Management (Deployment Role) - Create instances from templates at a specific site. - Assign instances to areas. -- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **Bulk assignment** supported: select multiple attributes and assign a data connection to all of them at once. Each row also exposes: +- Bind data connections — **per-attribute binding** where each attribute with a data source reference individually selects its data connection from the site's available connections. **The binding rows come from the instance's flattened configuration** (`IFlatteningPipeline.FlattenAndValidateAsync(id, validateScripts:false)`), so every data-sourced attribute the deploy gate will check appears — composed feature-module members (`LeftMESReceiver.MoveInFlag`, `LeftReactorSide.LeakTest.DeltaVac`, …) included. (Until 2026-08-20 the panel was built from the template's *direct* attributes only, which left 264 of a composed QA instance's 280 bindings invisible and made the instance undeployable from the UI; design `docs/plans/2026-08-20-grouped-connection-bindings-design.md`.) Only a genuine flatten *failure* falls back to the direct-attribute list, with a visible warning banner; a validation failure (e.g. unbound attributes — the very thing the panel exists to fix) is not a reason to fall back. + - **Grouped by module.** Rows are grouped by the first segment of the canonical name — `(instance)` for direct attributes first, then one collapsible group per composed module in name order; nested modules fold into their first segment (`LeftReactorSide` shows *incl. LeakTest*) and members keep their nested tail (`LeakTest.DeltaVac`). The pure grouping/inference/apply logic is `BindingGroupBuilder` (CentralUI `Services/`), unit-tested without rendering the page. + - **Group header = connection + Object + Apply.** Each group header carries a connection select and an **Object** input; **Apply to n** rewrites every member of the group to `.` on that connection (a blank Object clears the overrides so the template default applies; surrounding whitespace and a trailing `.` are tolerated). The Object input is pre-filled with the **inferred** shared prefix when every overridden member agrees (`MESReceiver_022.MoveInFlag` + `MESReceiver_022.ScanStateCmd` ⇒ `MESReceiver_022`; nested object paths such as `Left_002.DelmiaReceiver` infer too); when members disagree, or an override does not end in its member tail, the group shows a **mixed** pill and the input's placeholder invites an object to unify them. Apply edits the in-memory maps only — **Save Bindings** persists every row (hidden rows included) through the existing `SetConnectionBindings` upsert, and the header shows an `n changed · unsaved` pill until then. + - **Pills and filters.** The card header totals unbound rows and mixed groups; each group shows `n unbound` (red) or `bound`, plus `mixed`; unbound rows render highlighted. A **Show only unbound** filter hides bound rows and fully-bound groups; **Expand all** opens every group. The legacy all-groups connection select (**Assign all to…**) is retained. A composed member whose template default is the bare member name (`MoveInFlag`) carries a ⚠ marker — such a default cannot be a real address and the deploy gate will reject the instance until an override (or a group Object) is set. + - **Template default** column shows `ResolvedAttribute.DefaultDataSourceReference` — the reference as resolved from the template chain *before* the per-instance override — so the operator always sees both the default and the effective override. **Test Bindings** (button label carries the testable-row count) reads every bound row with an effective path, composed rows included. - **Override** — optional per-attribute OPC UA node id (or other protocol address). When set, replaces the template's `DataSourceReference` at flattening time; when blank, the template default is used. The greyed placeholder shows the template default for context. - **Browse…** — opens the OPC UA Tag Browser dialog, populated live from the site's OPC UA server via `BrowseOpcUaNodeCommand`. Visible only when the row's connection uses the OPC UA protocol; disabled until a connection is picked on that row. The dialog lazy-loads the address space, supports manual node-id entry as a fallback, and remains usable when the site or its OPC UA session is offline (the manual-paste field stays active even on error). The dialog adds: - **Load more** — when a browse level is truncated, a "Load more" affordance fetches the next page using the server's continuation point (`BrowseNext`); an expired continuation point falls back to a fresh browse. diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index b4160753..f9a9d27a 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -292,7 +292,7 @@ Each flattened configuration output includes a **revision hash** (computed from - Staleness detection: comparing the deployed revision to the current template-derived revision without a full diff. - Diff correlation: ensuring diffs are computed against a consistent baseline. -The override flows into the flattened attribute's `DataSourceReference` and therefore participates in the revision hash — changes to an instance's binding overrides re-deploy as expected. +The override flows into the flattened attribute's `DataSourceReference` and therefore participates in the revision hash — changes to an instance's binding overrides re-deploy as expected. The pre-override value is preserved alongside it as **`ResolvedAttribute.DefaultDataSourceReference`** (2026-08-20, grouped connection bindings): populated by `FlatteningService.ApplyConnectionBindings` for *every* data-sourced attribute, bound or not (equal to `DataSourceReference` when no override is set; null for non-data-sourced attributes). It is central-side presentation data for the Configure page's *Template default* column and is **deliberately excluded from the revision hash** — `RevisionHashService` projects through `HashableAttribute`, which does not carry it — so adding it changed no deployed revision (pinned by `DefaultDataSourceReferenceTests`). The revision hash covers the resolved attributes, alarms, scripts, connections, **and native alarm source bindings** (`ResolvedNativeAlarmSource`: canonical name, connection name, source reference, condition filter, and locked state). Native alarm sources were folded into the hash in migration TemplateEngine-011; the hashable projection is populated null-when-empty and the canonical serializer omits null properties, so every native-source-free configuration keeps its byte-identical pre-migration hash. **Migration note:** native-source-bearing instances will report stale once after upgrade; redeploy clears it — deliberate. diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs index 9c4494dc..47c2e600 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs @@ -62,9 +62,9 @@ public static class InstanceCommands "[attributeName, dataConnectionId, dataSourceReferenceOverride] " + "(the 3rd element overrides the attribute's data-source reference; " + "pass null or omit it to use the template default). " + - "NOTE: this REPLACES all bindings for the instance — include the " + - "override on every entry that needs one, or omitting it clears any " + - "previously-set override.", + "NOTE: this UPSERTS — listed attributes are updated or added, unlisted " + + "ones keep their binding; omitting the 3rd element on a listed entry " + + "clears that attribute's previously-set override.", Required = true }; diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md index 923bca73..6595f35c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md @@ -683,9 +683,39 @@ scadabridge --url instance set-bindings --id 42 \ --bindings '[["Speed",7,"ns=2;s=Reactor.Speed"],["Mode",7]]' ``` -> **Note:** this command **replaces** all bindings for the instance. Include the -> override on every entry that needs one — omitting the third element clears any -> previously-set override for that attribute. +> **Note:** this command **upserts** — each listed attribute is updated (or added); +> attributes *not* listed keep their existing binding. Within a listed entry, omitting +> the third element clears any previously-set override for that attribute. + +#### `instance bind-module` + +Rebind every member of one composed module (or the instance's direct attributes) to a +new object path in a single command — the CLI counterpart of the Configure page's +per-group **Apply**. Reads the instance's current bindings (`GetInstance`), rewrites the +module's rows client-side to `.`, and upserts only those rows +(`SetConnectionBindings`), so every other module is left untouched. + +```sh +scadabridge --url instance bind-module --id --module --connection --object [--dry-run] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Instance ID | +| `--module` | yes | Module key = first segment of the canonical attribute name (e.g. `RightMESReceiver`, which also covers nested `RightMESReceiver.LeakTest.*`). Use `(instance)` for the direct (dot-less) attributes | +| `--connection` | yes | Data connection ID every member is bound to | +| `--object` | yes | Object path the members hang off (`MESReceiver_022`, `Left_002.DelmiaReceiver`); a trailing `.` is tolerated. An **empty string clears** the overrides so the template defaults apply | +| `--dry-run` | no | Print the resulting bindings without saving | + +```sh +# Repoint the right-hand MES receiver module of instance 1 to the MESReceiver_022 object. +scadabridge --url instance bind-module --id 1 --module RightMESReceiver --connection 1 --object MESReceiver_022 +``` + +The module must already have at least one binding row on the instance — the command +rewrites existing rows, it does not discover the template's members. A module with no +bindings yet must be seeded once with `instance set-bindings` (or bound once in the Central +UI); an unknown module fails with `UNKNOWN_MODULE` and lists the modules that do have rows. #### `instance set-overrides`