+
+ The resolver walked your selection's dependency graph and produced the closure
+ below. Items under Auto-included were pulled in because the items you
+ ticked reference them; unticking
+ Include all dependencies exports the seed alone.
+
+
+
+ { _includeDependencies = ((bool?)e.Value) == true; await ReresolveAsync(); }" />
+
+
+
+
+
+
Selected by you
+
+ @foreach (var t in _resolved.Templates.Where(t => seedTemplateIds.Contains(t.Id)).OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - Template: @t.Name
+ }
+ @foreach (var s in _resolved.SharedScripts.Where(s => seedSharedScripts.Contains(s.Id)).OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - SharedScript: @s.Name
+ }
+ @foreach (var e in _resolved.ExternalSystems.Where(e => seedExternalSystems.Contains(e.Id)).OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - ExternalSystem: @e.Name
+ }
+ @foreach (var d in _resolved.DatabaseConnections.OrderBy(d => d.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - DatabaseConnection: @d.Name
+ }
+ @foreach (var n in _resolved.NotificationLists.OrderBy(n => n.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - NotificationList: @n.Name
+ }
+ @foreach (var s in _resolved.SmtpConfigs.OrderBy(s => s.Host, StringComparer.OrdinalIgnoreCase))
+ {
+ - SmtpConfig: @s.Host
+ }
+ @foreach (var k in _resolved.ApiKeys.OrderBy(k => k.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - ApiKey: @k.Name
+ }
+ @foreach (var m in _resolved.ApiMethods.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - ApiMethod: @m.Name
+ }
+
+
+
+
Auto-included (dependencies)
+ @if (autoTemplates.Count + autoShared.Count + autoExternals.Count + _resolved.TemplateFolders.Count == 0)
+ {
+
No additional dependencies.
+ }
+ else
+ {
+
+ @foreach (var f in _resolved.TemplateFolders.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - TemplateFolder: @f.Name
+ }
+ @foreach (var t in autoTemplates.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - Template: @t.Name
+ }
+ @foreach (var s in autoShared.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - SharedScript: @s.Name
+ }
+ @foreach (var e in autoExternals.OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ - ExternalSystem: @e.Name
+ }
+
+ }
+
+
+
+
+
+
+
+
+ };
+
+ // ============================================================
+ // Step 3 — Encrypt
+ // ============================================================
+ private RenderFragment RenderStepEncrypt() => __builder =>
+ {
+ var strength = PassphraseStrength(_passphrase);
+ var strengthLabel = strength switch
+ {
+ 0 => "Too short",
+ 1 => "Weak",
+ 2 => "Fair",
+ 3 => "Good",
+ _ => "Strong",
+ };
+ var strengthColor = strength switch
+ {
+ <= 1 => "bg-danger",
+ 2 => "bg-warning",
+ 3 => "bg-info",
+ _ => "bg-success",
+ };
+
+
+ @if (_secretCount > 0)
+ {
+
+ @_secretCount secret @(_secretCount == 1 ? "field" : "fields")
+ will be encrypted (external-system credentials, SMTP credentials, and database
+ connection strings).
+
+ }
+ else
+ {
+
+ The selected closure contains no secret fields, but the bundle's content
+ payload will still be encrypted in full when a passphrase is supplied.
+
+ }
+
+
+
+
+
+
Strength: @strengthLabel · minimum 8 characters.
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(_passphraseConfirm) && _passphrase != _passphraseConfirm)
+ {
+
Passphrases do not match.
+ }
+
+
+
+
+ Export without encryption…
+
+
+
+ @if (_showUnencryptedConfirm)
+ {
+
+
Unencrypted export — the bundle will contain all secret fields
+ in plaintext. Anyone with the file can read external-system credentials, SMTP
+ passwords, and database connection strings. The audit log will record this as
+
UnencryptedBundleExport.
+
+
+
+
+
+ }
+
+
+
+
+
+
+ };
+
+ // ============================================================
+ // Step 4 — Download
+ // ============================================================
+ private RenderFragment RenderStepDownload() => __builder =>
+ {
+
+ @if (_downloadInProgress)
+ {
+
+
Building bundle…
+ }
+ else if (_downloadError != null)
+ {
+
+ Export failed: @_downloadError
+
+
+ }
+ else
+ {
+
+ Bundle ready. Your browser is downloading the file.
+
+
+
+ - Filename
+ @_downloadFilename
+
+ - Size
+ - @FormatBytes(_downloadSize)
+
+ - SHA-256
+ @_downloadSha256
+
+ - Encryption
+ -
+ @if (_exportUnencrypted)
+ {
+ Unencrypted (audited as
UnencryptedBundleExport)
+ }
+ else
+ {
+ AES-256-GCM with PBKDF2-SHA256
+ }
+
+
+
+
+ }
+
+ };
+
+ private static string FormatBytes(long bytes)
+ {
+ if (bytes < 1024) return $"{bytes} B";
+ if (bytes < 1024 * 1024) return $"{bytes / 1024.0:0.0} KB";
+ return $"{bytes / (1024.0 * 1024.0):0.00} MB";
+ }
+}
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs
new file mode 100644
index 0000000..c968e47
--- /dev/null
+++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs
@@ -0,0 +1,427 @@
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.Options;
+using Microsoft.JSInterop;
+using ScadaLink.CentralUI.Auth;
+using ScadaLink.Commons.Entities.ExternalSystems;
+using ScadaLink.Commons.Entities.InboundApi;
+using ScadaLink.Commons.Entities.Notifications;
+using ScadaLink.Commons.Entities.Scripts;
+using ScadaLink.Commons.Entities.Templates;
+using ScadaLink.Commons.Interfaces.Repositories;
+using ScadaLink.Commons.Interfaces.Transport;
+using ScadaLink.Commons.Types.Transport;
+using ScadaLink.Transport;
+using ScadaLink.Transport.Export;
+
+namespace ScadaLink.CentralUI.Components.Pages.Design;
+
+///