diff --git a/docs/components/CLI.md b/docs/components/CLI.md index a4de4d14..c8e38d96 100644 --- a/docs/components/CLI.md +++ b/docs/components/CLI.md @@ -40,7 +40,7 @@ A request lacking the required role exits with code `2` (authorization failure). ### Output formats -Every command accepts `--format json` (default) or `--format table`. JSON output goes to stdout and is formatted with indented, camelCase JSON. Table output renders a padded plain-text table derived from the response JSON — arrays become rows, single objects become a two-column `Property / Value` table. Errors are always written as `{ "error": "...", "code": "..." }` to stderr, regardless of format. +Every command accepts `--format json` (default) or `--format table`. For JSON output, successful server responses are written to stdout verbatim — the server controls the JSON shape and no re-serialization is applied. Locally-constructed output (errors, `debug stream` events) is serialized by `OutputFormatter` with indentation and camelCase. Table output renders a padded plain-text table derived from the response JSON — arrays become rows, single objects become a two-column `Property / Value` table. Errors are always written as `{ "error": "...", "code": "..." }` to stderr, regardless of format. ## Architecture @@ -106,13 +106,13 @@ scadabridge instance deploy --id 7 | Group | Subcommands | Role required | |-------|-------------|---------------| | `template` | `list`, `get`, `create`, `update`, `delete`, `validate`; `attribute add/update/delete`; `alarm add/update/delete`; `script add/update/delete`; `composition add/delete`; `native-alarm-source add/list/remove` | `Design` | -| `instance` | `list`, `get`, `create`, `deploy`, `enable`, `disable`, `delete`, `set-bindings`; `native-alarm-source set/clear` | `Deployment` | +| `instance` | `list`, `get`, `create`, `set-bindings`, `set-overrides`, `alarm-override set/delete/list`, `native-alarm-source set/clear`, `set-area`, `diff`, `deploy`, `enable`, `disable`, `delete` | `Deployment` | | `site` | `list`, `get`, `create`, `delete`, `deploy-artifacts`; `area list/create/update/delete` | `Admin` | | `deploy` | `instance`, `artifacts`, `status` | `Deployment` | | `data-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` / `Deployment` | | `external-system` | `list`, `get`, `create`, `update`, `delete` | `Design` | | `notification` | `list`, `get`, `create`, `update`, `delete`; `smtp list/update` | `Design` / `Admin` | -| `security` | `api-key list/create/update/delete`; `role-mapping list/create/update/delete`; `scope-rule list/add/delete` | `Admin` | +| `security` | `api-key list/create/update/delete/set-methods`; `role-mapping list/create/update/delete`; `scope-rule list/add/delete` | `Admin` | | `shared-script` | `list`, `get`, `create`, `update`, `delete` | `Design` | | `db-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` | | `api-method` | `list`, `get`, `create`, `update`, `delete` | `Design` | @@ -238,7 +238,7 @@ The `audit-log` command group was renamed to `audit-config` in M8 of Audit Log ( ### Bundle timeout -`bundle export` and `bundle import` use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side export or import may still be running. Re-try with a smaller selection or check the central node logs. +`bundle export`, `bundle preview`, and `bundle import` all use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side operation may still be running. Re-try with a smaller selection or check the central node logs. ## Related Documentation diff --git a/docs/components/CentralUI.md b/docs/components/CentralUI.md index a34b3780..08e8de53 100644 --- a/docs/components/CentralUI.md +++ b/docs/components/CentralUI.md @@ -13,10 +13,10 @@ The component code lives in `src/ZB.MOM.WW.ScadaBridge.CentralUI/`, split into: - `Components/Layout/` — `MainLayout`, `LoginLayout`, and `NavMenu` (the policy-gated rail navigation). - `Components/Pages/` — pages, grouped by nav section: `Admin/`, `Audit/`, `Deployment/`, `Design/`, `Monitoring/`, `Notifications/`, `SiteCalls/`. - `Components/Shared/` — reusable non-page components (`DataTable`, `MonacoEditor`, `ToastNotification`, `SessionExpiry`, dialog infrastructure, and others). -- `Components/Health/` — KPI tile components for Notification Outbox, Site Calls, and Audit Log. +- `Components/Health/` — KPI tile components: `SiteCallKpiTiles` and `AuditKpiTiles`. Notification Outbox KPIs are rendered inline in `Health.razor`, not as a separate tile component. - `Components/Audit/` — the `AuditFilterBar`, `AuditResultsGrid`, `AuditDrilldownDrawer`, and execution-tree components used by the Audit Log page. -- `ScriptAnalysis/` — Roslyn-backed script analysis service and Minimal API endpoint (`/api/centralui/script/analyze`) used by the Monaco editor. -- `Services/` — scoped UI services: `AuditLogQueryService`, `AuditLogExportService`, `BrowseService`, `BindingTester`, and `SiteScopeService`. +- `ScriptAnalysis/` — Roslyn-backed script analysis service and Minimal API endpoint group (`/api/script-analysis`) used by the Monaco editor, exposing seven POST endpoints: `/diagnostics`, `/completions`, `/hover`, `/signature-help`, `/format`, `/inlay-hints`, and `/run`. +- `Services/` — scoped UI services: `AuditLogQueryService`, `AuditLogExportService`, `BrowseService`, and `BindingTester`. The single DI entry point is `ServiceCollectionExtensions.AddCentralUI`, registered only by the central-role Host composition root. `EndpointExtensions.MapCentralUI` wires the Minimal API endpoints and the Razor component routes onto the ASP.NET Core pipeline. @@ -74,7 +74,7 @@ CentralUI pages read and write the configuration database directly through `ICen | Monitoring | Health: all; Event Logs + Parked: Deployment | Health Dashboard, Event Logs, Parked Messages | | Audit | `OperationalAudit` | Audit Log, Configuration Audit Log | -`LoginLayout` is a stripped layout used by `/login`; it also renders `SessionExpiry` so expiry detection is active there without causing a redirect loop (the component no-ops when already on the login page). +`LoginLayout` is a minimal layout used by `/login` — it renders only `@Body` with no nav sidebar, session-expiry watchdog, or dialog host. `SessionExpiry` is rendered exclusively in `MainLayout`; it self-guards against redirect loops by checking whether the current URL is already the login page (`IsOnLoginPage`) and skipping polling if so. `DialogHost` — a single instance rendered in `MainLayout` — is the rendering target for all modal dialogs raised via the `IDialogService` / `DialogService` scoped service. Pages call `Dialog.ConfirmAsync` or `Dialog.PromptAsync` and await the result without managing modal state themselves. @@ -110,7 +110,7 @@ KPI queries go through `CommunicationService` (Notification Outbox and Site Call ### Script analysis endpoint -`ScriptAnalysisService` compiles user script fragments as Roslyn C# Scripting globals against `SandboxScriptHost` (template/shared scripts) or `InboundScriptHost` (Inbound API methods). It surfaces diagnostics and completions in the shape Monaco's provider APIs expect. Diagnostics are cached by code hash via `IMemoryCache` (200-entry limit) to short-circuit repeated requests for the same content. `ScriptAnalysisEndpoints` exposes `POST /api/centralui/script/analyze` and is called by the `MonacoEditor` component's Blazor JS interop module on a 500 ms debounce. +`ScriptAnalysisService` compiles user script fragments as Roslyn C# Scripting globals against `SandboxScriptHost` (template/shared scripts) or `InboundScriptHost` (Inbound API methods). It surfaces diagnostics and completions in the shape Monaco's provider APIs expect. Diagnostics are cached by code hash via `IMemoryCache` (200-entry limit) to short-circuit repeated requests for the same content. `ScriptAnalysisEndpoints` registers an endpoint group under `/api/script-analysis` with seven POST endpoints (`/diagnostics`, `/completions`, `/hover`, `/signature-help`, `/format`, `/inlay-hints`, `/run`); each is called by the Monaco JS providers in `monaco-init.js` on a 500 ms debounce (diagnostics) or on editor events (hover, completions, etc.). The sandbox enforces the script trust model: a `SemanticModel` check raises a diagnostic for any use of forbidden APIs (`System.IO`, `Process`, `Thread`, reflection, raw networking). @@ -205,7 +205,7 @@ The idle timeout is a sliding window — any authenticated HTTP request (includi ### Script editor shows no diagnostics -`ScriptAnalysisService` uses Roslyn from in-process `IMemoryCache` (200-entry size limit). If the cache is evicted under memory pressure, the next keystroke re-analyzes. If diagnostics never appear, check that `ScriptAnalysisEndpoints` is registered (it is mapped via `MapCentralUI`), and that the Monaco JS module's network requests to `/api/centralui/script/analyze` are completing successfully. +`ScriptAnalysisService` uses Roslyn from in-process `IMemoryCache` (200-entry size limit). If the cache is evicted under memory pressure, the next keystroke re-analyzes. If diagnostics never appear, check that `ScriptAnalysisEndpoints` is registered (it is mapped via `MapCentralUI`), and that the Monaco JS module's network requests to `/api/script-analysis/diagnostics` (and related endpoints) are completing successfully. ## Related Documentation diff --git a/docs/components/ManagementService.md b/docs/components/ManagementService.md index 83769615..78849711 100644 --- a/docs/components/ManagementService.md +++ b/docs/components/ManagementService.md @@ -39,7 +39,7 @@ Authorization is a two-level check. `GetRequiredRole` maps each command type to |------|----------| | `Administrator` | Site management, role mappings, API key management, scope rules, `QueryAuditLogCommand`, `PreviewBundle`, `ImportBundle` | | `Designer` | Template authoring (members, folders, compositions), external systems, data connections, notification lists, shared scripts, database connections, inbound API methods, `ExportBundle` | -| `Deployer` | Instance lifecycle, connection bindings, overrides, deployments, debug snapshot, parked message queries | +| `Deployer` | Instance lifecycle, connection bindings, overrides, deployments, debug snapshot, `RetryParkedMessageCommand`, `DiscardParkedMessageCommand` | | _(any authenticated user)_ | Read-only list/get queries, health summary | Within `Deployer` commands, `EnforceSiteScope` applies a second check: users whose role mapping carries `PermittedSiteIds` can only touch instances and sites within their permitted set. Administrators and system-wide deployers (empty `PermittedSiteIds`) are unrestricted. A violation throws `SiteScopeViolationException`, which `MapFault` converts to `ManagementUnauthorized`. @@ -50,7 +50,7 @@ Within `Deployer` commands, `EnforceSiteScope` applies a second check: users who ### Audit contract -Mutating handlers that call repositories directly invoke `AuditAsync` (backed by `IAuditService`) after a successful write. Handlers that delegate to domain services — `TemplateService`, `InstanceService`, `DeploymentService`, `ArtifactDeploymentService`, `TemplateFolderService`, `SharedScriptService` — do not call `AuditAsync`; those services audit internally. This avoids double-logging. SMTP configuration and API key responses project out secrets before the audit entry is written. +Mutating handlers that call repositories directly invoke `AuditAsync` (backed by `IAuditService`) after a successful write. Most handlers that delegate to a domain service — `TemplateService`, `DeploymentService`, `ArtifactDeploymentService`, `TemplateFolderService`, `SharedScriptService` — do not call `AuditAsync`; those services audit internally, avoiding double-logging. However, some delegating handlers also call `AuditAsync` directly: `HandleCreateInstance` delegates to `InstanceService.CreateInstanceAsync` and then calls `AuditAsync` itself. SMTP configuration and API key responses project out secrets before the audit entry is written. ## Architecture @@ -168,7 +168,7 @@ The `ManagementActor` is also reachable from any `ClusterClient` that has a cont | Template members | `AddTemplateAttribute`, `UpdateTemplateAttribute`, `DeleteTemplateAttribute`, `AddTemplateAlarm`, `UpdateTemplateAlarm`, `DeleteTemplateAlarm`, `AddTemplateNativeAlarmSource`, `UpdateTemplateNativeAlarmSource`, `DeleteTemplateNativeAlarmSource`, `ListTemplateNativeAlarmSources`, `AddTemplateScript`, `UpdateTemplateScript`, `DeleteTemplateScript`, `AddTemplateComposition`, `DeleteTemplateComposition` | Designer (mutations) | | Template folders | `ListTemplateFolders`, `CreateTemplateFolder`, `RenameTemplateFolder`, `MoveTemplateFolder`, `DeleteTemplateFolder`, `MoveTemplateToFolder` | Designer (mutations) | | Instances | `ListInstances`, `GetInstance`, `CreateInstance`, `MgmtDeployInstance`, `MgmtEnableInstance`, `MgmtDisableInstance`, `MgmtDeleteInstance`, `SetConnectionBindings`, `SetInstanceOverrides`, `SetInstanceArea`, `SetInstanceAlarmOverride`, `DeleteInstanceAlarmOverride`, `ListInstanceAlarmOverrides`, `SetInstanceNativeAlarmSourceOverride`, `DeleteInstanceNativeAlarmSourceOverride`, `ListInstanceNativeAlarmSourceOverrides` | Deployer (mutations) | -| Sites & areas | `ListSites`, `GetSite`, `CreateSite`, `UpdateSite`, `DeleteSite`, `ListAreas`, `CreateArea`, `UpdateArea`, `DeleteArea` | Administrator (mutations) | +| Sites & areas | `ListSites`, `GetSite`, `CreateSite`, `UpdateSite`, `DeleteSite`, `ListAreas`, `CreateArea`, `UpdateArea`, `DeleteArea` | Administrator (site mutations); Designer (`CreateArea`, `UpdateArea`, `DeleteArea`) | | Data connections | `ListDataConnections`, `GetDataConnection`, `CreateDataConnection`, `UpdateDataConnection`, `DeleteDataConnection` | Designer (mutations) | | External systems | `ListExternalSystems`, `GetExternalSystem`, `CreateExternalSystem`, `UpdateExternalSystem`, `DeleteExternalSystem`, `ListExternalSystemMethods`, `GetExternalSystemMethod`, `CreateExternalSystemMethod`, `UpdateExternalSystemMethod`, `DeleteExternalSystemMethod` | Designer (mutations) | | Notification lists / SMTP | `ListNotificationLists`, `GetNotificationList`, `CreateNotificationList`, `UpdateNotificationList`, `DeleteNotificationList`, `ListSmtpConfigs`, `UpdateSmtpConfig` | Designer (mutations) | @@ -178,7 +178,7 @@ The `ManagementActor` is also reachable from any `ClusterClient` that has a cont | Security | `ListRoleMappings`, `CreateRoleMapping`, `UpdateRoleMapping`, `DeleteRoleMapping`, `ListApiKeys`, `CreateApiKey`, `UpdateApiKey`, `DeleteApiKey`, `SetApiKeyMethods`, `ListScopeRules`, `AddScopeRule`, `DeleteScopeRule` | Administrator | | Deployments | `MgmtDeployArtifacts`, `QueryDeployments`, `GetDeploymentDiff` | Deployer | | Health | `GetHealthSummary`, `GetSiteHealth` | Any authenticated user | -| Remote queries | `QueryEventLogsCommand`, `QueryParkedMessagesCommand`, `RetryParkedMessageCommand`, `DiscardParkedMessageCommand`, `DebugSnapshotCommand` | Deployer | +| Remote queries | `QueryEventLogsCommand`, `QueryParkedMessagesCommand` (any authenticated user); `RetryParkedMessageCommand`, `DiscardParkedMessageCommand`, `DebugSnapshotCommand` (Deployer) | Varies | | Audit (legacy) | `QueryAuditLog` | Administrator | | Transport | `ExportBundle` (Designer), `PreviewBundle`, `ImportBundle` (Administrator) | Varies | diff --git a/docs/components/Transport.md b/docs/components/Transport.md index f9605016..ff002939 100644 --- a/docs/components/Transport.md +++ b/docs/components/Transport.md @@ -27,7 +27,7 @@ bundle.scadabundle `manifest.json` is always plaintext so the import wizard can display source provenance and artifact counts before the operator supplies a passphrase. `BundleManifest` carries: `BundleFormatVersion`, `SchemaVersion`, `CreatedAtUtc`, `SourceEnvironment`, `ExportedBy`, `ScadaBridgeVersion`, `ContentHash` (`sha256:` of the raw content bytes), optional `Encryption` metadata, a `Summary` (artifact counts by type), and a `Contents` list (one `ManifestContentEntry` per artifact with its `dependsOn` edges). -`BundleFormatVersion` is an integer gate: the importer hard-refuses any value higher than `TransportOptions.SchemaVersionMajor` (default `1`). Unknown entity types in `Contents` produce a preview-row classification of "unsupported" rather than aborting the whole import. +`BundleFormatVersion` is an integer gate: the importer requires `BundleFormatVersion` to equal `ManifestBuilder.CurrentBundleFormatVersion` (currently `1`) and rejects any other value — higher or lower — with `ManifestValidationResult.UnsupportedFormatVersion`. `TransportOptions.SchemaVersionMajor` is not read during import. Unknown entity types in `Contents` produce a preview-row classification of "unsupported" rather than aborting the whole import. ### Encrypted vs plaintext bundles @@ -95,8 +95,8 @@ public sealed class BundleExporter : IBundleExporter | Edge | Mechanism | |------|-----------| | Template composes Template | `TemplateComposition.ComposedTemplateId` (BFS over composition graph) | -| Template references SharedScript | Name-scan of `TemplateScript.Code` and `TemplateAttribute.Value` | -| Template references ExternalSystem | Name-scan of `TemplateScript.Code` and `TemplateAttribute.DataSourceReference` | +| Template references SharedScript | Name-scan of `TemplateScript.Code`, `TemplateAttribute.Value`, and `TemplateAttribute.DataSourceReference` | +| Template references ExternalSystem | Name-scan of `TemplateScript.Code`, `TemplateAttribute.DataSourceReference`, and `TemplateAttribute.Value` | | ApiMethod references SharedScript | Name-scan of `ApiMethod.Script` | | Template folder ancestor chain | Always included regardless of `IncludeDependencies` | @@ -131,7 +131,7 @@ _sessionStore.ClearUnlockFailures(manifest.ContentHash); **Phase 2 — `PreviewAsync`**: deserializes the session's plaintext bytes to `BundleContentDto` and calls `ArtifactDiff.Compare*` methods for each entity type. Diff results use `ConflictKind` (`Identical`, `Modified`, `New`, `Blocker`). `Modified` items carry a `FieldDiffJson` payload with changed-field names and old/new values; script bodies record a line-count delta rather than full text to keep the diff compact. `DetectBlockersAsync` scans script bodies for unresolvable `SharedScript` or `ExternalSystem` name references. -**Phase 3 — `ApplyAsync`**: runs semantic validation first (a name-resolution scan plus the full `SemanticValidator` from `TemplateEngine`), then applies all resolutions inside one EF transaction. The correlation GUID is set on `IAuditCorrelationContext.BundleImportId` before any writes so that every `IAuditService.LogAsync` call during the apply picks it up automatically. A two-pass flush handles forward references: the first `SaveChangesAsync` materializes identity values for new rows; `ResolveAlarmScriptLinksAsync` and `ResolveCompositionEdgesAsync` run afterward inside the same transaction. On failure, the transaction rolls back, `BundleImportId` is cleared, and a `BundleImportFailed` row is written outside the rolled-back transaction before the exception propagates. +**Phase 3 — `ApplyAsync`**: runs semantic validation first (a name-resolution scan plus the full `SemanticValidator` from `TemplateEngine`), then applies all resolutions inside one EF transaction. The correlation GUID is set on `IAuditCorrelationContext.BundleImportId` before any writes so that every `IAuditService.LogAsync` call during the apply picks it up automatically. Three `SaveChangesAsync` calls handle forward references: an intermediate flush inside `ApplyTemplatesAsync` materializes folder identity values so that template `FolderId` foreign keys can be wired correctly; a second flush after all `Apply*` helpers materializes row identities before `ResolveAlarmScriptLinksAsync` and `ResolveCompositionEdgesAsync` run; a third flush commits the `BundleImported` audit row just before `CommitAsync`. All three flushes operate inside the same outer transaction. On failure, the transaction rolls back, `BundleImportId` is cleared, and a `BundleImportFailed` row is written outside the rolled-back transaction before the exception propagates. ```csharp // From BundleImporter.ApplyAsync — correlation + transaction pattern @@ -197,7 +197,7 @@ After import, template changes propagate to deployed instances through revision- | Key | Default | Description | |-----|---------|-------------| | `SourceEnvironment` | `"scadabridge"` | Environment label stamped in `manifest.json` and used in export filenames. | -| `SchemaVersionMajor` | `1` | Maximum `bundleFormatVersion` this node accepts at import. | +| `SchemaVersionMajor` | `1` | Major schema version stamped in exported manifests. Not read by the importer; import version-gating uses `ManifestBuilder.CurrentBundleFormatVersion` directly. | | `BundleSessionTtlMinutes` | `30` | TTL for an in-progress import session. | | `MaxBundleSizeMb` | `100` | Upload size cap; enforced before any decompression. | | `MaxBundleEntryCount` | `4` | Maximum ZIP entries (a valid bundle has exactly 2). | @@ -223,7 +223,7 @@ After import, template changes propagate to deployed instances through revision- ### Bundle upload rejected at format version check -`LoadAsync` throws `NotSupportedException` when `manifest.json` carries a `bundleFormatVersion` higher than `TransportOptions.SchemaVersionMajor`. The bundle was exported from a newer ScadaBridge version. Upgrade the target cluster or re-export from a compatible version. +`LoadAsync` throws `NotSupportedException` when `manifest.json` carries a `bundleFormatVersion` that does not equal `ManifestBuilder.CurrentBundleFormatVersion` (currently `1`). Any non-matching value — whether higher or lower — is rejected. Upgrade the target cluster or re-export from a version that produces format version `1`. ### Content hash mismatch on upload diff --git a/docs/components/TreeView.md b/docs/components/TreeView.md index 904c62f8..212fb434 100644 --- a/docs/components/TreeView.md +++ b/docs/components/TreeView.md @@ -84,7 +84,14 @@ The Data Connections page binds a two-level Site → Connection tree with `Stora } - @if (node.Kind == DcNodeKind.DataConnection) + @if (node.Kind == DcNodeKind.Site) + { + + } + else {