docs: create-time connection bindings (Central UI, DeploymentManager preview flatten, CLI cross-ref)

Claude-Session: https://claude.ai/code/session_01G2GHPnbbKGvyyfbr2XBRVH
This commit is contained in:
Joseph Doherty
2026-08-21 06:14:55 -04:00
parent 80b9ab2d57
commit a26edffd4c
6 changed files with 98 additions and 11 deletions
+1
View File
@@ -127,6 +127,7 @@ spec for each is `docs/requirements/Component-<Name>.md`, and `README.md` carrie
- Deployment identity: unique deployment ID + revision hash for idempotency.
- Per-instance operation lock covers all mutating commands (deploy, disable, enable, delete).
- **Bulk site deployment (`DeploymentService.DeploySiteAsync`, arch-review WP2.5)** redeploys every out-of-date instance at a site through one flatten session (a template chain shared by N instances is walked once) in three phases — serial Prepare, bounded-parallel Send (`SiteDeploymentMaxParallelism`, default 4), serial Finalize — still **not all-or-nothing**: any one instance's failure doesn't block the rest. **Disabled instances are SKIPPED by bulk deploy as of 2026-08-19 (arch-review P7-2)** — they are reported in the summary's additive `SkippedInstances` list, never flattened/sent/re-enabled (single-instance deploy remains the explicit operator path that deploys+enables a Disabled instance); **Up-to-date instances are ALSO skipped as of 2026-08-20 (arch-review P7-6)** — a redeploy is not idempotent at the site (it resets static overrides and clears native alarm state), so `DeploySiteAsync` pre-filters through `IStaleInstanceProbe` (trustworthy since P7-4 fixed its memo timing) and reports unchanged instances in `SkippedInstances` with reason `up-to-date`; the filter is conservative (every uncertainty deploys: missing snapshot, `ShouldQuerySiteBeforeRedeploy`, null/faulting probe). Additive `MgmtDeploySiteCommand.Force` / CLI `deploy site --force` restores the unconditional push for what central cannot see (restored volume, rebuilt node) — **Disabled still wins under `--force`**. Also P7-5: the three phases share one scoped DbContext and a failed `SaveChanges` leaves the entity tracked, so one permanent failure used to cascade to every later instance and strand records `InProgress`; `MarkDeploymentFailedAsync` now detaches pending changes and retries the `Failed` write once on the clean tracker; Prepare's per-instance reconcile also runs under a short `SiteDeploymentReconcileTimeout` (5s) with a once-per-batch unreachable-site circuit-breaker, so a down site no longer costs 30s × N serial before the first send (P7-3). The `PendingDeployment` staging row (5-minute TTL) is written per-instance immediately before that instance's round-trip in the Send phase, not upfront in Prepare, so a large batch's tail instances don't have their TTL clock already half-spent by the time they're reached.
- **Create Instance binds module groups at creation** (2026-08-21): the create page's Connection Bindings card flattens template+site through the additive `IFlatteningPipeline.PreviewAsync` (an unsaved instance — no validation, no hash, nothing persisted, instance repo never touched) and reuses `BindingGroupBuilder`; `NodeBrowserDialog.SelectableNodeClasses` lets it pick **Object** nodes (default stays Variable-only for every existing caller). Create = `CreateInstanceAsync` → `SetConnectionBindingsAsync` (the existing upsert, no new server surface) → always navigate to Configure with `?created=1&bound=N|&bindError=…`, because the row exists whether or not the second call succeeded. Partial binding is legal: a group missing a connection or an Object is skipped and stays unbound. Design: `docs/plans/2026-08-21-create-instance-bindings-design.md`.
- Site-side apply is all-or-nothing per instance.
- System-wide artifact version skew across sites is supported.
- Last-write-wins for concurrent template editing (no optimistic concurrency on templates).
@@ -1,6 +1,6 @@
# Create Instance — bind connections at creation (design)
**Date:** 2026-08-21 · **Status:** approved in conversation, awaiting implementation plan
**Date:** 2026-08-21 · **Status:** **implemented** 2026-08-21 — plan `docs/plans/2026-08-21-create-instance-bindings.md`
**Mockup:** Claude artifact "Create Instance Bindings" (https://claude.ai/code/artifact/125dc749-c923-4977-a02a-eeac826e6f26)
**Builds on:** `docs/plans/2026-08-20-grouped-connection-bindings-design.md` (Configure page groups, `BindingGroupBuilder`, per-module Object apply)
@@ -1,15 +1,78 @@
{
"planPath": "docs/plans/2026-08-21-create-instance-bindings.md",
"tasks": [
{"id": 1, "subject": "Task 1: IFlatteningPipeline.PreviewAsync (DeploymentManager)", "status": "pending"},
{"id": 2, "subject": "Task 2: CreateBindingPlanner — Prefill + tail stripping + binding list (pure)", "status": "pending"},
{"id": 3, "subject": "Task 3: NodeBrowserDialog.SelectableNodeClasses (additive)", "status": "pending"},
{"id": 4, "subject": "Task 4: Create page — code-behind state + preview load", "status": "pending", "blockedBy": [1, 2]},
{"id": 5, "subject": "Task 5: Create page — Connection Bindings card markup + Browse", "status": "pending", "blockedBy": [3, 4]},
{"id": 6, "subject": "Task 6: Create → bindings → navigate to Configure; Configure one-shot notice", "status": "pending", "blockedBy": [5]},
{"id": 7, "subject": "Task 7: Live check on the docker rig", "status": "pending", "blockedBy": [6]},
{"id": 8, "subject": "Task 8: Docs", "status": "pending", "blockedBy": [6]},
{"id": 9, "subject": "Task 9: Solution build + impacted suites", "status": "pending", "blockedBy": [1,2,3,4,5,6,7,8]}
{
"id": 1,
"subject": "Task 1: IFlatteningPipeline.PreviewAsync (DeploymentManager)",
"status": "completed"
},
{
"id": 2,
"subject": "Task 2: CreateBindingPlanner \u2014 Prefill + tail stripping + binding list (pure)",
"status": "completed"
},
{
"id": 3,
"subject": "Task 3: NodeBrowserDialog.SelectableNodeClasses (additive)",
"status": "completed"
},
{
"id": 4,
"subject": "Task 4: Create page \u2014 code-behind state + preview load",
"status": "completed",
"blockedBy": [
1,
2
]
},
{
"id": 5,
"subject": "Task 5: Create page \u2014 Connection Bindings card markup + Browse",
"status": "completed",
"blockedBy": [
3,
4
]
},
{
"id": 6,
"subject": "Task 6: Create \u2192 bindings \u2192 navigate to Configure; Configure one-shot notice",
"status": "completed",
"blockedBy": [
5
]
},
{
"id": 7,
"subject": "Task 7: Live check on the docker rig",
"status": "pending",
"blockedBy": [
6
]
},
{
"id": 8,
"subject": "Task 8: Docs",
"status": "pending",
"blockedBy": [
6
]
},
{
"id": 9,
"subject": "Task 9: Solution build + impacted suites",
"status": "pending",
"blockedBy": [
1,
2,
3,
4,
5,
6,
7,
8
]
}
],
"lastUpdated": "2026-08-21T00:00:00Z"
}
}
+6
View File
@@ -118,6 +118,12 @@ Central cluster only. Sites have no user interface.
### Instance Management (Deployment Role)
- Create instances from templates at a specific site.
- **Connection Bindings at creation** (`InstanceCreate`, design `docs/plans/2026-08-21-create-instance-bindings-design.md`). Once a template AND a site are chosen, the create form grows a Connection Bindings card showing the SAME module groups the Configure page shows, so one Create can yield a deployable instance instead of always landing on Configure with 100% of the rows unbound. The groups come from `IFlatteningPipeline.PreviewAsync(templateId, siteId)` — a flatten of an *unsaved* instance with no overrides and no bindings — fed through the same `BindingGroupBuilder` Configure uses. **Group headers only**: per-member editing stays on Configure.
- **Bind all to** — a connection + root object, then **Prefill groups** sets the connection on every group and fills *blank* Objects only: the `(instance)` group gets the root verbatim; a module group whose template defaults already carry a shared prefix keeps it (pill: *from template*); any other module group is suggested `{root}.{groupKey}` (pill: *suggested*). An Object the operator typed is never overwritten. Provenance pills exist because a suggestion is a guess about the address space — a sibling-object layout needs correcting before Create.
- **Browse…** per group (and for the root) opens the shared `NodeBrowserDialog` with `SelectableNodeClasses = {Object, Variable}` — the create page binds a module to its **container** node, which the Variable-only default could not select. Picking a leaf *under* the module still works: `CreateBindingPlanner.ObjectFromPick` strips a trailing `.{memberTail}` matching any member of that group. Enabled only for browsable protocols (OPC UA, MxGateway), the same gate Configure applies.
- **Partial binding is legal.** A group with both a connection and a non-blank Object contributes `{object}.{memberTail}` for every member; a group missing either is simply skipped and stays unbound for Configure. The footer states the count that will be saved and how many groups stay unbound (and that Deploy is rejected until they are bound). A **Skip bindings** checkbox restores the pre-2026-08-21 behaviour exactly.
- **Create is two calls, and the instance survives the second failing.** `CreateInstanceAsync` then `SetConnectionBindingsAsync` (an upsert — the same command the Configure page's Save uses; no new server-side surface). The page then ALWAYS navigates to `/deployment/instances/{id}/configure?created=1` plus either `&bound=N` or `&bindError=<message>`, and Configure renders a one-shot notice from those query params. Leaving the operator on a stale create form after the row already exists would invite a duplicate-name retry.
- A preview flatten that fails (or throws) degrades to a warning inside the card and suppresses the binding save — the instance is still creatable, and Configure remains the authority.
- 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. **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.
@@ -252,6 +252,18 @@ remains the authoritative compile**: instance deployment re-flattens with
`validateScripts: true` (the default), and a script that fails to compile blocks
the deployment (see Site-Side Apply Atomicity).
### Preview flatten (no instance row)
`IFlatteningPipeline.PreviewAsync(templateId, siteId)` flattens a template against
a site **as if** an instance with no overrides and no bindings existed there. It
returns the `FlattenedConfiguration` alone — no validation, no revision hash,
nothing persisted, and the instance repository is never touched. It exists for the
Central UI's Create Instance page, which needs the instance's module groups (and
therefore a real flatten, composed members included) *before* the instance row is
created; see `Component-CentralUI.md` → Instance Management. It shares the template
chain + nested-composition walk with `FlattenAndValidateAsync` (`LoadTemplateGraphAsync`),
so the two cannot drift, and each call gets a private `FlattenSession`.
## Deployable Artifacts
A deployment to a site includes the flattened instance configuration plus any system-wide artifacts that have changed:
+5
View File
@@ -561,6 +561,11 @@ scadabridge --url <url> instance create --name <string> --template-id <int> --si
| `--site-id` | yes | Site where the instance will run |
| `--area-id` | no | Area within the site |
The command creates the instance with **no** connection bindings. The Central UI's
Create Instance page can bind the module groups at creation time (design
2026-08-21); the CLI equivalent is a follow-up `instance set-bindings` /
`instance bind-module` against the new instance id.
#### `instance native-alarm-source set`
Override an inherited native alarm source binding for a single instance (upsert). Blank