# Grouped Connection Bindings — design **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 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 `