|
|
|
@@ -13,7 +13,7 @@ Central cluster only. Sites have no user interface.
|
|
|
|
|
- **Framework**: Blazor Server (ASP.NET Core). UI logic executes on the server, updates pushed to the browser via SignalR.
|
|
|
|
|
- Keeps the entire stack in C#/.NET, consistent with the rest of the system (Akka.NET, EF Core).
|
|
|
|
|
- SignalR provides built-in support for real-time UI updates.
|
|
|
|
|
- **Dialog service confirm labels.** Every confirmation goes through `IDialogService.ConfirmAsync(title, message, danger, confirmLabel)`, rendered by the single `DialogHost` in `MainLayout`. `danger` selects **styling only** (`btn-danger`); `confirmLabel` names the action. When no label is supplied the button falls back to "Delete" for a danger confirm and "Confirm" otherwise — a convenience for the ~20 genuine delete call sites, and the reason the Health dashboard's *Trigger failover* dialog used to offer a red **Delete** button (deferred-work register row 28). **Any destructive action that is not literally a delete must pass `confirmLabel`**: failover → *Fail over*, secured-write approve/reject → *Approve* / *Reject*, instance disable → *Disable*, and every parked-message / notification / cached-call discard → *Discard*.
|
|
|
|
|
- **Dialog service confirm labels.** Every confirmation goes through `IDialogService.ConfirmAsync(title, message, danger, confirmLabel)`, rendered by the single `DialogHost` in `MainLayout`. `danger` selects **styling only** (`btn-danger`); `confirmLabel` names the action. When no label is supplied the button falls back to "Delete" for a danger confirm and "Confirm" otherwise — a convenience for the ~20 genuine delete call sites, and the reason the Health dashboard's *Trigger failover* dialog used to offer a red **Delete** button (deferred-work register row 28). **Any destructive action that is not literally a delete must pass `confirmLabel`**: failover → *Fail over*, secured-write approve/reject → *Approve* / *Reject*, instance disable → *Disable*, and every parked-message / notification / cached-call discard → *Discard*. The two most-cited verbs are pinned by test: `Monitoring/FailoverConfirmLabelTests` (failover, plus that an unlabelled danger confirm still defaults to "Delete") and `Deployment/TopologyDisableConfirmLabelTests` (Topology's instance **Disable** reads "Disable" and stays red, while its **Delete** still reads "Delete"). Both drive a real `DialogService` behind a real `DialogHost`, so they pin what the operator reads, not the argument a substitute received.
|
|
|
|
|
- **Dialog confirm severity — three rungs, not two** (deferred-work register row 75). `DialogSeverity` drives the confirm button's colour: `Normal` → `btn-primary`, `Caution` → `btn-warning`, `Destructive` → `btn-danger`. Only `Destructive` inherits the "Delete" label fallback above; an unlabelled `Caution` reads *Confirm*, because a caution action is by definition not a delete.
|
|
|
|
|
- **Why a middle rung exists.** The boolean was a two-state flag for a three-state concept, so a consequential-but-not-destructive action had to choose between looking like a delete and looking like the safe default. The **Secured Writes** page is where that bit: *Approve* and *Reject* both rendered as identical red buttons, so at the moment of a two-person-control decision the colour carried **no discriminating information at all** — and the affirmative half of the control wore the delete affordance.
|
|
|
|
|
- **The decision.** *Approve* is `Caution` (amber): approving executes a real write to live plant equipment, so it is not a benign primary action, but it is not destroying anything either. *Reject* **keeps** `Destructive` (red): it is terminal and irreversible for the request — the Operator must resubmit — and holding it red is what keeps the two opposite decisions visually distinct. Dropping *Reject* to a neutral button was considered and rejected: it would simply invert the bias, making declining look like the safe default.
|
|
|
|
@@ -45,7 +45,7 @@ Central cluster only. Sites have no user interface.
|
|
|
|
|
### Template Authoring (Design Role)
|
|
|
|
|
- The `/design/templates` page uses a **split-pane layout**: a folder/template tree sidebar on the left and the editor on the right.
|
|
|
|
|
- The tree shows nested `TemplateFolder` entities with their templates underneath; composition children render inline beneath their owning template (right-click "Open composed template" reveals and selects the target). Compositions are shown by **effective set** (own + inherited): a derived/composed member surfaces the slots it inherits from its base — e.g. `LeakTest` composed onto base `ReactorSide` appears under each derived `…ReactorSide` member — badged **"inherited"** and read-only (Rename/Delete are offered on the base's own slot, not the inherited copy).
|
|
|
|
|
- **Per-kind context menus** on folder, template, and composition nodes expose the relevant operations (new folder, new template, rename, move, delete, move to folder). Root-level folders also carry a context menu. **Folder sibling reorder** is done via **Move up / Move down** menu items (M9/T23, `ReorderTemplateFolderCommand`); drag-drop is **not implemented** (permanently deferred). Tree expansion state persists in `sessionStorage`, and deep links (`/design/templates/{id}`) reveal and select the target node.
|
|
|
|
|
- **Per-kind context menus** on folder, template, and composition nodes expose the relevant operations (new folder, new template, rename, move, delete, move to folder). A Template node also offers **"New Instance…"**, which navigates to `/deployment/instances/create?templateId={id}` with the picker field prefilled; it is **gated on the `Deployer` role claim** (exactly what `RequireDeployment` requires) so the item never leads a Designer-only user to a 403. Root-level folders also carry a context menu. **Folder sibling reorder** is done via **Move up / Move down** menu items (M9/T23, `ReorderTemplateFolderCommand`); drag-drop is **not implemented** (permanently deferred). Tree expansion state persists in `sessionStorage`, and deep links (`/design/templates/{id}`) reveal and select the target node.
|
|
|
|
|
- A **search box** above the tree (M9/T22) filters visible nodes by substring match; it is wired to `TemplateFolderTree.Filter`.
|
|
|
|
|
- The `TemplateEdit` page shows a read-only **"Inherited members" panel** (M9/T26) listing the full multi-level effective inherited member set (origin, locked state, merged HiLo config) resolved by `GetResolvedTemplateMembersCommand` / `TemplateInheritanceResolver`. A **"Base changed" banner** appears when the resolver's staleness summary indicates the parent template has changed since the child was last edited. This is read-only — no "update-derived" mutation is exposed; the child is redeployed through the normal flow to pick up base changes.
|
|
|
|
|
- Create, edit, and delete templates.
|
|
|
|
@@ -118,6 +118,8 @@ Central cluster only. Sites have no user interface.
|
|
|
|
|
|
|
|
|
|
### Instance Management (Deployment Role)
|
|
|
|
|
- Create instances from templates at a specific site.
|
|
|
|
|
- **The template is chosen from the Templates folder tree, not a flat dropdown** (`InstanceCreate`, design `docs/plans/2026-08-21-template-picker.md`). The Template field is a **read-only, folder-qualified path box** ("Controllers / Conveyor / MES Line Controller", rendered by `TemplatePathFormatter`) with a **Choose…** button; both open `TemplatePickerDialog`, a `IDialogService.ShowAsync` body wrapping the **same shared `TemplateFolderTree`** the Templates page renders — its own search box, expand/collapse all, templates-only selection (folders just toggle), double-click to pick, and a footer showing the pending path. It opens **expanded to and highlighting the current pick** via the tree's additive `RevealKeys` parameter (ancestors of the named `f:`/`t:` keys start expanded when no filter is active; an active filter's own reveal set still wins, and callers that pass nothing are unaffected). A flat `<select>` was unusable once the catalogue grew past a screenful and showed no folder context at all.
|
|
|
|
|
- **`?templateId=` preselects.** The page takes a `TemplateId` query parameter, which is what the Templates tree's "New Instance…" navigates with; combined with the existing `?siteId=`, the Connection Bindings card below is populated on first render. An id that no longer exists leaves the field blank and shows *"Template N no longer exists — choose another."* rather than failing the page. With no templates at all, **Choose…** is disabled and the field points at the Templates page.
|
|
|
|
|
- **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.
|
|
|
|
|