refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,439 @@
@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.
*@
<div class="container-fluid mt-3">
<h4 class="mb-3">Import Bundle</h4>
@* Step indicator — five numbered pills, mirrors TransportExport. *@
<nav aria-label="Import wizard steps" class="mb-4">
<ol class="list-unstyled d-flex flex-wrap gap-3 mb-0 small">
<li class="@StepClass(ImportWizardStep.Upload)">
<span class="badge rounded-pill me-1">1</span> Upload
</li>
<li class="@StepClass(ImportWizardStep.Passphrase)">
<span class="badge rounded-pill me-1">2</span> Passphrase
</li>
<li class="@StepClass(ImportWizardStep.Diff)">
<span class="badge rounded-pill me-1">3</span> Diff
</li>
<li class="@StepClass(ImportWizardStep.Confirm)">
<span class="badge rounded-pill me-1">4</span> Confirm
</li>
<li class="@StepClass(ImportWizardStep.Result)">
<span class="badge rounded-pill me-1">5</span> Result
</li>
</ol>
</nav>
@if (_errorMessage != null)
{
<div class="alert alert-danger" data-testid="error-message">@_errorMessage</div>
}
@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;
}
</div>
@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 =>
{
<div>
<p class="text-body-secondary">
Select a <code>.scadabundle</code> 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.
</p>
<div class="mb-3">
<label for="bundle-input" class="form-label">Bundle file</label>
<InputFile id="bundle-input" OnChange="OnFileSelectedAsync"
class="form-control" accept=".scadabundle,application/zip" />
<div class="form-text">
Maximum bundle size: @Options.Value.MaxBundleSizeMb MB.
</div>
</div>
@if (_uploadInProgress)
{
<div class="text-muted small fst-italic">Reading bundle…</div>
}
@if (_bundleTempPath is not null && _errorMessage is null)
{
@if (_session is not null)
{
<dl class="row small mt-3" data-testid="manifest-summary">
<dt class="col-sm-3">Source environment</dt>
<dd class="col-sm-9"><code>@_session.Manifest.SourceEnvironment</code></dd>
<dt class="col-sm-3">Exported by</dt>
<dd class="col-sm-9">@_session.Manifest.ExportedBy</dd>
<dt class="col-sm-3">Created</dt>
<dd class="col-sm-9">@_session.Manifest.CreatedAtUtc.ToString("u")</dd>
<dt class="col-sm-3">Content count</dt>
<dd class="col-sm-9">@_session.Manifest.Contents.Count items</dd>
<dt class="col-sm-3">SHA-256</dt>
<dd class="col-sm-9"><code class="small">@_session.Manifest.ContentHash</code></dd>
<dt class="col-sm-3">Encryption</dt>
<dd class="col-sm-9">
@if (_session.Manifest.Encryption is null)
{
<span class="text-warning">Unencrypted</span>
}
else
{
<span class="text-success">@_session.Manifest.Encryption.Algorithm</span>
}
</dd>
</dl>
}
else
{
<div class="alert alert-info mt-3" data-testid="encrypted-bundle-notice">
<strong>Encrypted bundle uploaded.</strong>
Click <strong>Next</strong> to enter the passphrase.
</div>
}
<div class="d-flex justify-content-end mt-3">
<button class="btn btn-primary" @onclick="GoFromUploadAsync">Next</button>
</div>
}
</div>
};
// ============================================================
// Step 2 — Passphrase
// ============================================================
private RenderFragment RenderStepPassphrase() => __builder =>
{
var maxAttempts = Options.Value.MaxUnlockAttemptsPerSession;
var attemptsLeft = Math.Max(0, maxAttempts - _failedUnlockAttempts);
<div>
<p class="text-body-secondary">
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.
</p>
<div class="mb-3">
<label for="import-passphrase" class="form-label">Passphrase</label>
<input id="import-passphrase" type="password" class="form-control"
autocomplete="current-password"
@bind="_passphrase" @bind:event="oninput" />
</div>
@if (_failedUnlockAttempts > 0)
{
<div class="alert alert-warning small" data-testid="unlock-attempts">
Failed unlock attempts: @_failedUnlockAttempts of @maxAttempts.
</div>
}
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-outline-secondary" @onclick="BackToUpload">Back</button>
<button class="btn btn-primary"
disabled="@(string.IsNullOrEmpty(_passphrase) || _uploadInProgress)"
@onclick="SubmitPassphraseAsync">
@(_uploadInProgress ? "Unlocking…" : "Unlock")
</button>
</div>
</div>
};
// ============================================================
// Step 3 — Diff & resolve conflicts
// ============================================================
private RenderFragment RenderStepDiff() => __builder =>
{
if (_preview is null || _resolutions is null)
{
<div class="alert alert-warning">No preview available — please go back and re-upload.</div>
return;
}
var (adds, overs, skips, renames, blockers) = CountResolutions();
var hasBlockers = _preview.Items.Any(i => i.Kind == ConflictKind.Blocker);
<div>
<p class="text-body-secondary">
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.
</p>
<div class="mb-3 d-flex flex-wrap gap-2 align-items-center" data-testid="bulk-actions">
<span class="small text-body-secondary">Apply to all modified:</span>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Skip)">Skip</button>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => BulkSet(ResolutionAction.Overwrite)">Overwrite</button>
</div>
<div class="table-responsive" style="max-height: 480px; overflow-y: auto;">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Status</th>
<th>Existing</th>
<th>Incoming</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var item in _preview.Items)
{
var key = (item.EntityType, item.Name);
var current = _resolutions[key];
<tr data-testid="diff-row">
<td><span class="badge bg-secondary">@item.EntityType</span></td>
<td>@item.Name</td>
<td>@RenderKindBadge(item)</td>
<td>@(item.ExistingVersion?.ToString() ?? "—")</td>
<td>@(item.IncomingVersion?.ToString() ?? "—")</td>
<td>@RenderResolutionControls(item, current)</td>
</tr>
@if (item.Kind == ConflictKind.Modified && !string.IsNullOrEmpty(item.FieldDiffJson))
{
<tr>
<td colspan="6">
<details>
<summary class="small">Field diff</summary>
<pre class="small mb-0"><code>@item.FieldDiffJson</code></pre>
</details>
</td>
</tr>
}
@if (item.Kind == ConflictKind.Blocker && !string.IsNullOrEmpty(item.BlockerReason))
{
<tr>
<td colspan="6">
<div class="alert alert-danger small mb-0">@item.BlockerReason</div>
</td>
</tr>
}
}
</tbody>
</table>
</div>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-outline-secondary" @onclick="BackToUpload">Back</button>
<div>
<span class="me-3 small text-body-secondary" data-testid="diff-summary">
@adds add · @overs overwrite · @skips skip · @renames rename · @blockers blocker
</span>
<button class="btn btn-primary"
disabled="@hasBlockers"
@onclick="GoToConfirm">
Next
</button>
</div>
</div>
</div>
};
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()),
};
<span class="badge @cls">@label</span>
};
private RenderFragment RenderResolutionControls(ImportPreviewItem item, ImportResolution current) => __builder =>
{
// Identical → forced Skip; New → forced Add; Blocker → no actions.
if (item.Kind == ConflictKind.Identical)
{
<span class="text-muted small">Skip</span>
return;
}
if (item.Kind == ConflictKind.New)
{
<span class="text-muted small">Add</span>
return;
}
if (item.Kind == ConflictKind.Blocker)
{
<span class="text-muted small">—</span>
return;
}
var key = (item.EntityType, item.Name);
<div class="d-flex flex-wrap gap-2 align-items-center">
@foreach (var action in new[] { ResolutionAction.Overwrite, ResolutionAction.Skip, ResolutionAction.Rename })
{
var inputId = $"res-{item.EntityType}-{item.Name}-{action}";
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="radio"
id="@inputId"
name="@($"res-{item.EntityType}-{item.Name}")"
checked="@(current.Action == action)"
@onchange="() => SetResolution(key, action)" />
<label class="form-check-label small" for="@inputId">@action</label>
</div>
}
@if (current.Action == ResolutionAction.Rename)
{
<input type="text" class="form-control form-control-sm"
style="max-width: 14rem;"
placeholder="New name"
value="@(current.RenameTo ?? string.Empty)"
@onchange="e => SetRenameTo(key, e.Value?.ToString())" />
}
</div>
};
// ============================================================
// Step 4 — Confirm
// ============================================================
private RenderFragment RenderStepConfirm() => __builder =>
{
if (_session is null)
{
<div class="alert alert-warning">No bundle session — please re-upload.</div>
return;
}
var (adds, overs, skips, renames, _) = CountResolutions();
var changeCount = adds + overs + renames;
<div>
<p class="text-body-secondary">
You are about to apply <strong>@changeCount</strong> change(s)
to this environment (@adds add · @overs overwrite · @skips skip · @renames rename).
</p>
<div class="alert alert-info small">
Affected instances will become stale and require redeployment via the
<a href="/deployment/deployments">Deployments</a> page.
</div>
<div class="mb-3">
<label for="confirm-env" class="form-label">
Type the source environment name <code>@_session.Manifest.SourceEnvironment</code> to confirm:
</label>
<input id="confirm-env" type="text" class="form-control"
@bind="_confirmEnvironmentText" @bind:event="oninput" />
</div>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-outline-secondary" @onclick="BackToDiff">Back</button>
<button class="btn btn-danger"
disabled="@(_confirmEnvironmentText != _session.Manifest.SourceEnvironment || _applyInProgress)"
@onclick="ApplyAsync">
@(_applyInProgress ? "Applying…" : "Apply Import")
</button>
</div>
</div>
};
// ============================================================
// Step 5 — Result
// ============================================================
private RenderFragment RenderStepResult() => __builder =>
{
<div>
@if (_validationErrors is not null && _validationErrors.Count > 0)
{
<div class="alert alert-danger" data-testid="validation-errors">
<strong>Bundle semantic validation failed.</strong>
<ul class="mb-0">
@foreach (var err in _validationErrors)
{
<li>@err</li>
}
</ul>
</div>
<button class="btn btn-outline-secondary" @onclick="BackToDiff">Back</button>
}
else if (_result is not null)
{
<div class="alert alert-success" data-testid="result-summary">
<strong>Import complete.</strong>
@_result.Added added · @_result.Overwritten overwritten ·
@_result.Skipped skipped · @_result.Renamed renamed.
</div>
<dl class="row small">
<dt class="col-sm-3">Bundle Import Id</dt>
<dd class="col-sm-9"><code>@_result.BundleImportId</code></dd>
</dl>
<div class="d-flex gap-3">
<a class="btn btn-outline-primary" href="/deployment/deployments">
View on Deployments →
</a>
<a class="btn btn-outline-secondary"
href="@($"/audit/configuration?bundleImportId={_result.BundleImportId}")">
Audit trail →
</a>
</div>
}
else
{
<div class="alert alert-danger">
Import failed. Please re-upload the bundle.
</div>
<button class="btn btn-outline-secondary" @onclick="BackToUpload">Back</button>
}
</div>
};
}