From eef77b66547b5ac8058c73ef28160fb8f07171af Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 15:44:40 -0400 Subject: [PATCH] =?UTF-8?q?docs(plans):=20grouped=20connection=20bindings?= =?UTF-8?q?=20design=20=E2=80=94=20Configure=20page=20sources=20binding=20?= =?UTF-8?q?rows=20from=20the=20flattened=20config=20(composed=20members=20?= =?UTF-8?q?were=20invisible),=20per-module=20Object=20bulk=20apply,=20CLI?= =?UTF-8?q?=20instance=20bind-module=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...8-20-grouped-connection-bindings-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/plans/2026-08-20-grouped-connection-bindings-design.md diff --git a/docs/plans/2026-08-20-grouped-connection-bindings-design.md b/docs/plans/2026-08-20-grouped-connection-bindings-design.md new file mode 100644 index 00000000..23a3f068 --- /dev/null +++ b/docs/plans/2026-08-20-grouped-connection-bindings-design.md @@ -0,0 +1,128 @@ +# Grouped Connection Bindings — design + +**Date:** 2026-08-20 · **Status:** approved, implementation pending · **Components:** Central UI (#15), Template Engine (flattening), CLI + +## Problem + +The Configure Instance page (`/deployment/instances/{id}/configure`) builds its Connection +Bindings table from `GetAttributesByTemplateIdAsync(templateId)` — the template's **own** +attributes only. Members contributed by composed feature modules (`LeftMESReceiver.*`, +`LeftReactorSide.LeakTest.*`, …) never appear, yet the deploy gate +(`ValidateConnectionBindingCompleteness`) requires every *flattened* data-sourced attribute to be +bound. For a composed template the UI therefore cannot make an instance deployable at all; on QA +(wonder-app-vq03) 264 of Z28061's 280 bindings are invisible on the page. The only working route +is the management `SetConnectionBindings` upsert (CLI `instance set-bindings` or a raw POST), +which today means hand-editing 26 rows per receiver module. + +Observed 2026-08-20 while remapping Z28061/Z28062/Z28063 MES and Delmia receivers on QA. + +Saving from the page is already safe: `_bindingSelections` is seeded from *all* existing bindings +and `SaveBindings` posts the whole dictionary, so the hidden rows survive a UI save. The gap is +visibility and editing, not data loss. + +## Decision + +1. **Source the bindings table from the flattened configuration.** The page already runs + `IFlatteningPipeline.FlattenAndValidateAsync(id, validateScripts: false)` for the alarm picker; + use the same result for bindings. One walk, the same walk the deploy gate uses, so the UI can + never disagree with the gate. +2. **Group rows by module** and give each group a connection picker + an **Object** box whose + Apply rewrites every member's override to `.`. This is exactly the bulk + operation performed by hand on QA. +3. **CLI parity:** `scadabridge instance bind-module` performs the same per-module rewrite through + the existing `SetConnectionBindings` upsert. No new server command. + +Alternatives rejected: walking the composition graph inside the page (duplicates the flattener's +inheritance/nesting/lock rules and drifts from it); a new "binding rows" management query (the +page already injects the pipeline; adds an RPC for nothing). + +## Template Engine (additive) + +`ResolvedAttribute` gains `string? DefaultDataSourceReference` — the data-source reference +**before** the instance binding is applied (the template/composed default). +`FlatteningService.ApplyConnectionBindings` sets it from `existing.DataSourceReference`; for +attributes with no binding it equals `DataSourceReference`. Central-side only — the flattened +config is not a wire contract here, and the field is **excluded from the revision hash** (the +hash input is unchanged, otherwise every deployed instance would read as stale after upgrade). +Pinned by a test that hashes a config with and without the field populated. + +## Central UI — Connection Bindings panel + +**Row source.** Flattened attributes where `DataSourceReference` (post-binding) or +`DefaultDataSourceReference` is non-empty. Row key = `CanonicalName` (path-qualified), which is +what `InstanceConnectionBinding.AttributeName` already stores for composed members. +If the flatten itself fails (not validation — a thrown/failed pipeline result) the panel falls back +to today's direct-attribute list and shows a warning banner naming the failure, so the page never +goes blank. + +**Grouping.** A pure helper, `BindingGroupBuilder` (CentralUI, no page dependencies), maps rows to +groups: + +- module key = canonical name minus its last segment; a nested module (`LeftReactorSide.LeakTest`) + folds into its **first** segment's group (`LeftReactorSide`) and its member tail keeps the nested + part (`LeakTest.DeltaVac`). Direct attributes (no dot) form the `(instance)` group, listed first. +- per group: member count, unbound count (rows with no connection), connection state + (`single` / `mixed` / `none`), and the **inferred object prefix** — the longest common prefix of + the members' effective references ending at a `.` boundary after stripping each member tail; + `null` when members disagree (`mixed`) or no row has an override. +- `ApplyObject(group, connectionId, object)` returns the new `(connectionId, override)` for every + member: `object + "." + memberTail`; an empty object clears the overrides (back to default) and + leaves connections as chosen. Whitespace trimmed; a trailing `.` on the object is tolerated. + +**Layout.** Groups collapsed by default, each header: caret · module name (with `incl. LeakTest` +style note when nested modules were folded) · count · connection `