# Component: Central UI ## Purpose The Central UI is a web-based management interface hosted on the central cluster. It provides all configuration, deployment, monitoring, and troubleshooting workflows for the SCADA system. There is no live machine data visualization — the UI is focused on system management, with the exception of on-demand debug views. ## Location Central cluster only. Sites have no user interface. ## Technology - **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*. 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. - Colour is never the only signal — the button always carries its verb (see the label contract above), so the mapping is an accessibility affordance, not the meaning. - **`DialogSeverity` is an additive overload**, not a replacement: the boolean form remains the signature for the ~34 existing call sites, which are genuinely two-state. Adding a fifth optional parameter to the boolean form instead would have silently un-matched every existing four-argument NSubstitute stub — the row-28 gotcha, where a stub that stops matching returns a null `Task` and the caller NREs with no compile error. Reach for the overload only when `Caution` is what you mean. ## Failover Behavior - A **load balancer** sits in front of the central cluster and routes to the active node. - On central failover, the Blazor Server SignalR circuit is interrupted. The browser automatically attempts to reconnect via SignalR's built-in reconnection logic. - Since sessions use **authentication cookies** carrying an embedded JWT (not server-side state), the user's authentication survives failover — the new active node validates the same cookie-embedded JWT. No re-login required if the token is still valid. - Active debug view streams and in-progress deployment status subscriptions are lost on failover and must be re-opened by the user. - Both central nodes share the same **ASP.NET Data Protection keys** (stored in the configuration database or shared configuration) so that tokens and anti-forgery tokens remain valid across failover. ## Real-Time Updates - **Debug view**: Real-time display of attribute values and alarm states via **gRPC streaming**. When the user opens a debug view, a `DebugStreamBridgeActor` on the central side opens a gRPC server-streaming subscription to the site's `SiteStreamGrpcServer` for the selected instance, then requests an initial `DebugViewSnapshot` over the central→site gRPC command channel (`SiteCommandService`, `QueryReply.DebugViewSnapshot`). Ongoing `AttributeValueChanged` and `AlarmStateChanged` events flow via the gRPC data stream to the bridge actor, which delivers them to the Blazor component via callbacks that call `InvokeAsync(StateHasChanged)` to push UI updates through the built-in SignalR circuit. - **Health dashboard**: Site status, connection health, error rates, and buffer depths update via a **10-second auto-refresh timer**. Since health reports arrive from sites every 30 seconds, a 10s poll interval catches updates within one reporting cycle without unnecessary overhead. - **Deployment status**: Pending/in-progress/success/failed transitions **push to the UI immediately** via SignalR (built into Blazor Server). No polling required for deployment tracking. ## Responsibilities - Provide authenticated access to all management workflows. - Enforce role-based access control in the UI (Admin, Design, Deployment with site scoping). - Present data from the configuration database, and from site clusters via remote queries. ## Workflows / Pages ### 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). 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. - **Template deletion** is blocked if any instances or child templates reference the template. The UI displays the references preventing deletion. - Manage template hierarchy (inheritance) — visual tree of parent/child relationships. - Manage composition — add/remove feature module instances within templates. **Naming collision detection** provides immediate feedback if composed modules introduce duplicate attribute, alarm, or script names. - Define and edit attributes, alarms, and scripts on templates. - **Member authoring dialogs are hosted, not page-embedded** (M10 residual, 2026-08-01): the template editor's Attribute, Alarm, Native Alarm Source, and Script forms all open through `IDialogService.ShowAsync`, so the single `DialogHost` in `MainLayout` owns the backdrop, focus trap, Escape, and focus restoration. Each form body is its own component beside the page (`TemplateAttributeDialog`, `TemplateAlarmDialog`, `TemplateNativeAlarmSourceDialog`, `TemplateScriptDialog`), following the `MoveDataConnectionDialog` pattern — the body renders validation and server errors **inline and stays open**, closing only on a successful save; persistence stays on the page (which owns `TemplateService`) behind an `OnSaveAsync` delegate. - **Native Alarms tab** (`TemplateEdit`): a tab alongside Attributes / Alarms / Scripts / Compositions that lists the template's **native alarm source bindings** — the OPC UA Alarms & Conditions / MxAccess Gateway sources whose alarm state the instance mirrors. Each binding carries Name, Connection, Source Reference, optional Condition Filter, Description, and a Lock flag. Add / edit / delete go through a **hosted dialog** (`TemplateNativeAlarmSourceDialog`): - **Name** — unique within the template (lock/inherit bookkeeping mirrors `TemplateAlarm`). - **Connection** — a dropdown filtered to **alarm-capable connections only** (OPC UA and MxGateway protocols). - **Source Reference** — the native key (OPC UA SourceNode / notifier nodeId, or MxAccess object/area). - **Condition Filter** (optional) — blank mirrors *all* conditions under the source. - **Description** (optional) and **Lock** (prevents instance-level override, like locked alarms/attributes). - CRUD is **repository-direct** (Blazor Server runs in-process against `ICentralUiRepository`); no Akka round-trip is needed for design-time authoring. - Set lock flags on attributes, alarms, and scripts. - Visual indicator showing inherited vs. locally defined vs. overridden members. - **On-demand validation**: A "Validate" action allows Design users to run comprehensive pre-deployment validation (flattening, naming collisions, script compilation, trigger references) without triggering a deployment. Provides early feedback during authoring. - **Last-write-wins** editing — no pessimistic locks or conflict detection on templates. ### Shared Script Management (Design Role) - Create, edit, and delete shared (global) scripts. - Shared scripts are not associated with any template. - On-demand validation (compilation check) available. ### External System Management (Design Role) - Define external system contracts: connection details, API method definitions (parameters, return types). - Define retry settings per external system (max retry count, fixed time between retries). - The external system detail page includes a **"Recent activity"** link that opens the Audit Log page pre-filtered to `Channel = ApiOutbound` and `Target` starts-with the system name — surfacing the system's recent outbound API audit history. ### Database Connection Management (Design Role) - Define named database connections: server, database, credentials. - Define retry settings per connection (max retry count, fixed time between retries). ### Notification List Management (Design Role) - Create, edit, and delete notification lists. - Each notification list has a **`Type`** — `Email` or `Sms` (SMS via Twilio shipped 2026-06-19; a Teams adapter was evaluated and dropped). The type determines the type-specific targets a list carries. - Manage recipients (name + email) within each `Email` list. - Configure SMTP settings. ### Site & Data Connection Management (Admin Role) - Create, edit, and delete site definitions, including gRPC node addresses (GrpcNodeA/GrpcNodeB). The legacy Akka node addresses (NodeA/NodeB) are still stored and editable but have had **no runtime consumer** since the ClusterClient→gRPC migration's Phase 4 — every central→site dial resolves the gRPC pair. The form labels them as legacy so an operator does not mistake them for a live setting. - Define data connections and assign them to sites (name, protocol type, connection details). - **Data connection form**: "Primary Endpoint Configuration" (required JSON text area) and optional "Backup Endpoint Configuration" (collapsible section, hidden by default, revealed via "Add Backup Endpoint" button; "Remove Backup" button when editing an existing backup). "Failover Retry Count" numeric input (default 3, min 1, max 20) is visible only when a backup endpoint is configured. - **Verify endpoint** (OPC UA): the OPC UA endpoint editor (in the data connection form) carries a **"Verify endpoint"** button that asks the target site to probe the configured endpoint — a temporary, short-lived connect against the live (or edited-but-unsaved) config. The result reports success or a typed failure kind (e.g. unreachable, untrusted certificate, server error). When the failure is an **untrusted server certificate**, the probe captures the cert (Subject / Issuer / Thumbprint / validity / DER) and the editor shows a detail panel with a **"Trust certificate"** button. The probe itself **never trusts** the cert — trusting is an explicit, Admin-gated action (see Server certificate management). After a Trust, Verify re-runs automatically and should then succeed. - **Data connection list page**: Shows Primary Config and Backup Config columns. Active Endpoint column populated from health reports. - **Connection live-status indicators** (M9/T25): the design DataConnections page polls `ConnectionHealthQueryService` (~10 s interval) which reads `SiteHealthReport.DataConnectionStatuses` already delivered by Health Monitoring; no new site-side code. Each connection row displays its current status badge (Connected / Disconnected / Unknown). - **Move data connection between sites** (M9/T24): a **Move…** action on a data connection opens `MoveDataConnectionDialog`. The underlying `MoveDataConnectionCommand` guards against: target site does not exist, name collision at the target, any `InstanceConnectionBinding` referencing the connection, and any `InstanceNativeAlarmSourceOverride.ConnectionNameOverride` reference at the source site's templates. Blocked connections display the blocking reason. - **Server certificate management** (`/design/connections/{id}/certificates`, Admin role): a per-connection page that lists the contents of the site's OPC UA trusted-peer and rejected certificate stores (Subject / Issuer / Thumbprint / validity / Trusted-or-Rejected status) with a **Remove** action. The page makes clear the store is **node-wide for the site** (shared by every site node), not per data connection — trusting or removing a certificate affects all OPC UA connections at that site. Trust and Remove are central commands relayed to **both** site nodes so the node-local PKI stores stay consistent across failover (see Component-SiteRuntime.md, Component-DataConnectionLayer.md). - The site detail page exposes a new **"Audit feed"** tab that hosts the Audit Log page pre-filtered to `Site = ` — an in-context view of every operational audit event for that site. ### Schema Library (Design Role) - A **Schema Library** page (M9/T32, under the Design nav group) provides CRUD for `SharedSchema` entries — named, reusable JSON Schema definitions. Each entry has a name (globally unique) and a JSON Schema body. - Schemas are referenced from parameter/return definitions using `{"$ref":"lib:Name"}`. The resolver expands refs at deploy-time, design-time validation, and inbound-API runtime (cycle and depth guards enforced; a dangling ref blocks deployment). - The Central UI also uses the `ParameterValueForm` (M9/T30) to render `object` and `list` parameters as typed nested inputs when a JSON Schema (or `$ref`) is attached to the parameter definition. Monaco-based JSON fields gain hover and completion (M9/T31) from the attached schema. ### Inbound API Management (Admin Role for keys, Design Role for methods) - Manage inbound API keys (create, enable / disable, delete) and define API methods (name, parameters, return values, approved keys, implementation script). - The API key detail page includes a **"Recent calls"** link that opens the Audit Log page pre-filtered to `Actor = ` and `Channel = ApiInbound` — surfacing the key's recent inbound-call audit history. ### Bundle Export / Import (Design Role for export, Admin Role for import) - `Export Bundle` (`/design/transport/export`) — multi-step wizard for exporting a `.scadabundle` artifact containing templates, shared scripts, external systems, and central-only configuration. Visible to users with `RequireDesign`. - `Import Bundle` (`/design/transport/import`) — multi-step wizard for uploading and applying a `.scadabundle` into the current environment, with per-artifact diff review and conflict resolution. Visible to users with `RequireAdmin`. ### Area Management (Designer or Deployer Role) - Areas are authored inside the Deployment Topology workflow (`Topology.razor`, `RequireDeployment`); managing them is gated **any-of [Designer, Deployer]**, not Administrator (arch-review C6). - Define hierarchical area structures per site. - Parent-child area relationships. - Assign areas when managing instances. ### 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 `