@page "/design/transport/import" @using ZB.MOM.WW.ScadaBridge.Security @using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport @using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport @using Microsoft.AspNetCore.Components.Forms @using Microsoft.Extensions.Options @using ZB.MOM.WW.ScadaBridge.Transport @using ZB.MOM.WW.ScadaBridge.Transport.Import @using System.Security.Cryptography @attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)] @* TransportImport wizard (Component #24, Task T22). A 5-step linear wizard: Step 1 — Upload : InputFile + manifest summary; LoadAsync without passphrase first. Step 2 — Passphrase : only shown for encrypted bundles; 3-strike lockout. Step 3 — Diff : conflict resolution (Add/Overwrite/Skip/Rename) per ImportPreviewItem. Step 4 — Confirm : type-the-environment-name guard. Step 5 — Result : ApplyAsync result + audit drilldown link. The page is Admin-only — Import touches central configuration globally. *@

Import Bundle

@* Step indicator — five numbered pills, mirrors TransportExport. *@ @if (_errorMessage != null) {
@_errorMessage
} @switch (_step) { case ImportWizardStep.Upload: @RenderStepUpload(); break; case ImportWizardStep.Passphrase: @RenderStepPassphrase(); break; case ImportWizardStep.Diff: @RenderStepDiff(); break; case ImportWizardStep.Confirm: @RenderStepConfirm(); break; case ImportWizardStep.Result: @RenderStepResult(); break; }
@code { private string StepClass(ImportWizardStep s) => s == _step ? "fw-semibold text-primary" : (int)s < (int)_step ? "text-success" : "text-muted"; // ============================================================ // Step 1 — Upload // ============================================================ private RenderFragment RenderStepUpload() => __builder => {

Select a .scadabundle file produced by an exporter on this or another cluster. The bundle's manifest will be validated immediately; encrypted bundles will prompt for a passphrase on the next step.

Maximum bundle size: @Options.Value.MaxBundleSizeMb MB.
@if (_uploadInProgress) {
Reading bundle…
} @if (_bundleTempPath is not null && _errorMessage is null) { @if (_session is not null) {
Source environment
@_session.Manifest.SourceEnvironment
Exported by
@_session.Manifest.ExportedBy
Created
@_session.Manifest.CreatedAtUtc.ToString("u")
Content count
@_session.Manifest.Contents.Count items
SHA-256
@_session.Manifest.ContentHash
Encryption
@if (_session.Manifest.Encryption is null) { Unencrypted } else { @_session.Manifest.Encryption.Algorithm }
} else {
Encrypted bundle uploaded. Click Next to enter the passphrase.
}
}
}; // ============================================================ // Step 2 — Passphrase // ============================================================ private RenderFragment RenderStepPassphrase() => __builder => { var maxAttempts = Options.Value.MaxUnlockAttemptsPerSession; var attemptsLeft = Math.Max(0, maxAttempts - _failedUnlockAttempts);

This bundle is encrypted. Enter the passphrase that was used to produce it. You have @attemptsLeft of @maxAttempts attempts before the upload must be restarted.

@if (_failedUnlockAttempts > 0) {
Failed unlock attempts: @_failedUnlockAttempts of @maxAttempts.
}
}; // ============================================================ // Step 3 — Diff & resolve conflicts // ============================================================ private RenderFragment RenderStepDiff() => __builder => { if (_preview is null || _resolutions is null) {
No preview available — please go back and re-upload.
return; } var (adds, overs, skips, renames, blockers) = CountResolutions(); var hasBlockers = _preview.Items.Any(i => i.Kind == ConflictKind.Blocker);
@RenderMapSection();

Review each artifact in the bundle and choose how it should be applied to this environment. Identical items are skipped automatically; new items default to Add; modified items require an explicit choice.

Apply to all modified:
@foreach (var item in _preview.Items) { var key = (item.EntityType, item.Name); var current = _resolutions[key]; @if (item.Kind == ConflictKind.Modified && !string.IsNullOrEmpty(item.FieldDiffJson)) { var lineDiff = TryExtractLineDiff(item.FieldDiffJson); } @if (item.Kind == ConflictKind.Blocker && !string.IsNullOrEmpty(item.BlockerReason)) { } }
Type Name Status Existing Incoming Action
@item.EntityType @item.Name @RenderKindBadge(item) @(item.ExistingVersion?.ToString() ?? "—") @(item.IncomingVersion?.ToString() ?? "—") @RenderResolutionControls(item, current)
Field diff @if (lineDiff is not null) {
Code changes
} else {
@item.FieldDiffJson
}
@item.BlockerReason
@adds add · @overs overwrite · @skips skip · @renames rename · @blockers blocker
}; // ============================================================ // Step 3 — Map sub-section (M8 E2) // ============================================================ // Shown only when the preview references source-environment sites/connections // the operator must resolve before import. For central-config-only bundles the // preview carries no required mappings and this renders nothing. private RenderFragment RenderMapSection() => __builder => { if (_preview is null) return; var hasSiteMappings = _preview.RequiredSiteMappings.Count > 0; var hasConnMappings = _preview.RequiredConnectionMappings.Count > 0; if (!hasSiteMappings && !hasConnMappings) { return; }
Resolve site & connection references This bundle references sites/connections from its source environment. Map each to an existing target, or create a new one.
@if (hasSiteMappings) {
Sites
@foreach (var rsm in _preview.RequiredSiteMappings) { var chosen = _siteChoices.TryGetValue(rsm.SourceSiteIdentifier, out var c) ? c : CreateNewValue; }
Source identifier Source name Map to target
@rsm.SourceSiteIdentifier @rsm.SourceSiteName
} @if (hasConnMappings) {
Connections
@foreach (var grp in _preview.RequiredConnectionMappings.GroupBy(m => m.SourceSiteIdentifier)) { var siteTarget = _siteChoices.TryGetValue(grp.Key, out var st) ? st : CreateNewValue; var targetConns = ConnectionsForChosenTarget(grp.Key);
Site @grp.Key @if (string.IsNullOrEmpty(siteTarget)) { new site }
@foreach (var rcm in grp) { var key = (rcm.SourceSiteIdentifier, rcm.SourceConnectionName); var chosenConn = _connectionChoices.TryGetValue(key, out var cc) ? cc : CreateNewValue; }
Source connection Map to target
@rcm.SourceConnectionName
} }
}; private RenderFragment RenderKindBadge(ImportPreviewItem item) => __builder => { var (cls, label) = item.Kind switch { ConflictKind.Identical => ("bg-secondary", "Identical"), ConflictKind.Modified => ("bg-warning text-dark", "Modified"), ConflictKind.New => ("bg-success", "New"), ConflictKind.Blocker => ("bg-danger", "Blocker"), _ => ("bg-light text-dark", item.Kind.ToString()), }; @label }; private RenderFragment RenderResolutionControls(ImportPreviewItem item, ImportResolution current) => __builder => { // Identical → forced Skip; New → forced Add; Blocker → no actions. if (item.Kind == ConflictKind.Identical) { Skip return; } if (item.Kind == ConflictKind.New) { Add return; } if (item.Kind == ConflictKind.Blocker) { return; } var key = (item.EntityType, item.Name);
@foreach (var action in new[] { ResolutionAction.Overwrite, ResolutionAction.Skip, ResolutionAction.Rename }) { var inputId = $"res-{item.EntityType}-{item.Name}-{action}";
} @if (current.Action == ResolutionAction.Rename) { }
}; // ============================================================ // Step 4 — Confirm // ============================================================ private RenderFragment RenderStepConfirm() => __builder => { if (_session is null) {
No bundle session — please re-upload.
return; } var (adds, overs, skips, renames, _) = CountResolutions(); var changeCount = adds + overs + renames;

You are about to apply @changeCount change(s) to this environment (@adds add · @overs overwrite · @skips skip · @renames rename).

Affected instances will become stale and require redeployment via the Deployments page.
}; // ============================================================ // Step 5 — Result // ============================================================ private RenderFragment RenderStepResult() => __builder => {
@if (_validationErrors is not null && _validationErrors.Count > 0) {
Bundle semantic validation failed.
} else if (_result is not null) {
Import complete. @_result.Added added · @_result.Overwritten overwritten · @_result.Skipped skipped · @_result.Renamed renamed.
Bundle Import Id
@_result.BundleImportId
View on Deployments → Audit trail →
} else {
Import failed. Please re-upload the bundle.
}
}; }