11-task plan (T0-T10) covering the sibling docker-env2/ directory:
SQL setup script + mount, Traefik config, central/site appsettings,
docker-compose, lifecycle scripts, .gitignore, READMEs and cross-refs,
verification checklist, and a manual smoke test. No application code
changes -- pure deploy tooling. Most tasks (T0-T9) are independent
and parallel-ready; T5 is gated on T0 + T4; T10 gates on all of T0-T9.
Brainstorming output for a sibling docker-env2/ tree that brings up a
minimal second cluster (2 central + 1 site x 2 nodes + Traefik) on the
same machine alongside the primary docker/ stack. Shares the existing
scadalink-net network and scadalink-mssql container but uses separate
logical databases (ScadaLinkConfig2 / ScadaLinkMachineData2) so the
Transport (#24) feature can be exercised end-to-end with real
cross-environment exports and imports.
The asserted 'LDAP credentials' tagline was deliberately removed from
Login.razor in f973f49 but the test was not updated alongside. Drop
the test — it asserts on UI text that no longer exists by design.
Add an unconditional alert-info banner in the Notification Lists fieldset
(Step 1) explaining that SMTP configurations are not auto-included as
dependencies and must be selected separately.
Add TimeSpan? MinTimeBetweenRuns to TemplateScriptDto and int MaxRetries /
TimeSpan RetryDelay to ExternalSystemDto; wire both directions in
EntitySerializer. Extends the existing script round-trip assertion and adds
Roundtrip_external_system_preserves_retry_config.
- NavMenu: move Import Bundle out of the nested RequireDesign/RequireAdmin
double-gate into the top-level Admin section so an Admin-only user sees it
without needing the Design role; Export Bundle stays in the Design section.
- TransportImport: inject IAuditService + ScadaLinkDbContext; emit a
BundleImportUnlockFailed audit row (best-effort, swallowed on failure) on
every wrong-passphrase attempt in SubmitPassphraseAsync, with attempt
number and error reason in afterState.
- docker central-node-a/b appsettings: add ScadaLink:Transport section with
SourceEnvironment = "docker-cluster" so the importer picks up a non-null
environment name in the audit trail.
- CentralUI.Tests: register IAuditService mock + SQLite in-memory
ScadaLinkDbContext in TransportImportPageTests to satisfy the two new injects.
Implements Task T21 of the Transport feature. A four-step Blazor wizard
(Select → Review → Encrypt → Download) under /design/transport/export,
gated on AuthorizationPolicies.RequireDesign:
1. Select — TemplateFolderTree (checkbox-mode) plus flat checkbox
lists for shared scripts, external systems, DB connections,
notification lists, SMTP configs, API keys, API methods.
2. Review — runs DependencyResolver, surfaces seed vs auto-included.
"Include all dependencies" toggle re-resolves on flip.
3. Encrypt — passphrase + confirm with strength meter, secret-count
warning over the resolved closure, explicit unencrypted
opt-out path (calls BundleExporter with passphrase=null
so the audit row tags UnencryptedBundleExport).
4. Download— calls IBundleExporter.ExportAsync, streams bytes to the
browser via JS interop (wwwroot/js/transport.js), displays
filename + size + SHA-256 + encryption status.
Source environment is sourced from new TransportOptions.SourceEnvironment
(bound from ScadaLink:Transport:SourceEnvironment, defaults "scadalink"),
filename pattern scadabundle-{env}-{yyyy-MM-dd-HHmmss}.scadabundle.
Tests (bUnit + policy): step 1 group rendering, step 2 dependency
expansion (Pump composes Motor), step 4 full walkthrough verifying
ExportAsync receives the selected ids + authenticated identity, and a
RequireDesign policy-deny test for users without the Design role. Also
unit-pins the filename-sanitisation contract.
Address one Blocker and three Important findings from code review of
2c34f12 (BundleImporter.ApplyAsync):
- BLOCKER: wrap RollbackAsync in nested try/catch so a rollback fault
does not swallow the BundleImportFailed audit row. Dispose the
failed transaction before the audit-write so the new SaveChangesAsync
uses a fresh implicit transaction instead of enlisting in the broken
one. Surface the rollback exception's message on the failure row
alongside the original cause, and swallow audit-write faults per the
design's best-effort-audit invariant. Add regression integration
test using a SQLite transaction interceptor that throws on rollback.
- Document re-entrancy assumption on IAuditCorrelationContext: scoped
lifetime, single circuit, concurrent imports within a shared scope
must serialize externally.
- Document repository audit responsibility on BundleImporter: repos
are thin EF wrappers; ApplyAsync writes audit rows explicitly. If
repos ever start emitting audit rows, the explicit calls here must
be removed to avoid double-logging.
- Document BundleSessionStore thread-safety: ConcurrentDictionary
primitives are safe under concurrent callers; BundleSession itself
is not thread-safe.
File-based, encrypted bundle export/import via the Central UI for
promoting templates, system artifacts, and central-only configuration
across environments. Site-scoped artifacts excluded. Per-artifact
conflict resolution; config-only import (user redeploys via existing
Deployments page). Per-entity audit rows correlated by BundleImportId.