fix(review): full code-review remediation — 5 High + Medium/Low across 16 modules

Remediation from the full per-module code review at 4307c381 (findings recorded
separately in code-reviews/).

Highs fixed:
- DeploymentManager-025/SiteRuntime-031: stop broadcasting notification lists + SMTP
  configs (incl. credentials) to sites; site purges already-persisted rows on apply
  (enforces the central-only delivery design; clears plaintext SMTP creds at rest).
- DataConnectionLayer-023: guard the native-alarm subscribe path against the
  mid-flight-unsubscribe adapter-feed leak (mirrors the DCL-021 tag-path fix).
- SiteEventLogging-024: normalize From/To query bounds to UTC (the -016 fix the
  audit trail claimed but never committed).
- KpiHistory-001: add an in-flight guard to the recorder sample tick.
- ScriptAnalysis-001: harden the trust analyzer's TPA-absent fallback (resolve
  forbidden anchors in the minimal reference set; warn on degraded mode) — anchors
  added to validation references only, never the compile gate.
(InboundAPI-026 left to the feat/ipsen-movein effort per owner decision.)

Medium/Low: DM-026 deterministic deploy-status tiebreaker; SR-027/028/029/030
native-alarm leak/phantom-active/delete-during-redeploy fixes; AL-013/014/016;
TE-024 (folder-mutation audit rows now persisted)/025; SF-025 gauge-provider
clear-on-stop; ESG-025/026; SEC-023/024/025; SCA-007/008/009; plus doc/test
accuracy COM-023/024, HOST-025/026, HM-024/025, NS-027/028.

Full-solution build 0 warnings; ~3560 tests across 18 touched suites green.
This commit is contained in:
Joseph Doherty
2026-06-20 17:55:12 -04:00
parent 4307c38117
commit fd618cf1dc
52 changed files with 2239 additions and 313 deletions
@@ -12,8 +12,16 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// <summary>
/// WP-7: System-wide artifact deployment.
/// Broadcasts artifacts (shared scripts, external systems, notification lists, DB connections,
/// data connections, and SMTP configurations) to all sites with per-site tracking.
/// Broadcasts artifacts (shared scripts, external systems, DB connections, and
/// data connections) to all sites with per-site tracking.
///
/// Notification lists and SMTP configuration are deliberately NOT shipped to
/// sites: notification delivery is central-only (sites store-and-forward to
/// central and never talk to SMTP), so no notification artifact or SMTP
/// credential is ever distributed to a site. The
/// <see cref="DeployArtifactsCommand"/> still carries the
/// <c>NotificationLists</c>/<c>SmtpConfigurations</c> fields for additive
/// message-contract compatibility, but central never populates them.
///
/// - Successful sites are NOT rolled back on other failures.
/// - Failed sites are retryable individually.
@@ -26,7 +34,6 @@ public class ArtifactDeploymentService
private readonly IDeploymentManagerRepository _deploymentRepo;
private readonly ITemplateEngineRepository _templateRepo;
private readonly IExternalSystemRepository _externalSystemRepo;
private readonly INotificationRepository _notificationRepo;
private readonly CommunicationService _communicationService;
private readonly IAuditService _auditService;
private readonly DeploymentManagerOptions _options;
@@ -39,7 +46,12 @@ public class ArtifactDeploymentService
/// <param name="deploymentRepo">Repository for deployment records.</param>
/// <param name="templateRepo">Repository for templates.</param>
/// <param name="externalSystemRepo">Repository for external systems.</param>
/// <param name="notificationRepo">Repository for notifications.</param>
/// <param name="notificationRepo">
/// DeploymentManager-025: retained on the signature for DI/source compatibility but
/// intentionally NOT consumed. Notification lists and SMTP configuration are
/// central-only and are never shipped to sites, so the artifact path must not read
/// the notification repository at all.
/// </param>
/// <param name="communicationService">Service for communicating with sites.</param>
/// <param name="auditService">Service for audit logging.</param>
/// <param name="options">Deployment manager options.</param>
@@ -59,7 +71,9 @@ public class ArtifactDeploymentService
_deploymentRepo = deploymentRepo;
_templateRepo = templateRepo;
_externalSystemRepo = externalSystemRepo;
_notificationRepo = notificationRepo;
// DeploymentManager-025: notificationRepo is deliberately not stored — notification
// lists and SMTP configs are central-only and are never fetched for shipping to sites.
_ = notificationRepo;
_communicationService = communicationService;
_auditService = auditService;
_options = options.Value;
@@ -98,15 +112,19 @@ public class ArtifactDeploymentService
/// <summary>
/// Builds a per-site <see cref="DeployArtifactsCommand"/> using a previously-fetched
/// snapshot of the global artifact sets (shared scripts, external systems + methods,
/// DB connections, notification lists, SMTP configurations). Only the per-site
/// data-connection query runs here.
/// DB connections). Only the per-site data-connection query runs here.
/// </summary>
/// <remarks>
/// DeploymentManager-023: separating the global fetch from the per-site build lets
/// <see cref="DeployToAllSitesAsync"/> issue the global queries exactly once across
/// the whole multi-site sweep, eliminating the N+1 re-query of shared scripts,
/// external systems, methods, DB connections, notification lists, and SMTP
/// configurations.
/// external systems, methods, and DB connections.
///
/// DeploymentManager-025: the command's <c>NotificationLists</c> and
/// <c>SmtpConfigurations</c> fields are always sent <c>null</c> — notification
/// delivery is central-only and no notification artifact or SMTP credential is
/// ever distributed to a site. The fields remain on the contract only for
/// additive compatibility.
/// </remarks>
private async Task<DeployArtifactsCommand> BuildDeployArtifactsCommandAsync(
int siteId,
@@ -125,30 +143,34 @@ public class ArtifactDeploymentService
globals.SharedScripts,
globals.ExternalSystems,
globals.DatabaseConnections,
globals.NotificationLists,
// DeploymentManager-025: notification lists are central-only — never shipped to sites.
NotificationLists: null,
dataConnectionArtifacts,
globals.SmtpConfigurations,
// DeploymentManager-025: SMTP config (incl. credentials) is central-only — never shipped to sites.
SmtpConfigurations: null,
DateTimeOffset.UtcNow);
}
/// <summary>
/// Fetches the system-wide artifact sets that are identical across every site —
/// shared scripts, external systems (with their methods serialized in), database
/// connections, notification lists, and SMTP configurations. Used by
/// <see cref="DeployToAllSitesAsync"/> to pre-load once before the per-site loop.
/// shared scripts, external systems (with their methods serialized in), and
/// database connections. Used by <see cref="DeployToAllSitesAsync"/> to pre-load
/// once before the per-site loop.
/// </summary>
/// <remarks>
/// DeploymentManager-023: the per-site artifact build path previously re-issued
/// every one of these queries per site (≈ N + M·N round trips for N sites
/// every one of these queries per site (≈ N + M·N round trips for N sites
/// and M external systems). Hoisting them here drops that to a single fetch.
///
/// DeploymentManager-025: notification lists and SMTP configurations are NOT
/// fetched here. Notification delivery is central-only, so they are never
/// shipped to sites — the artifact path must not even read them.
/// </remarks>
private async Task<GlobalArtifactSnapshot> FetchGlobalArtifactsAsync(CancellationToken cancellationToken)
{
var sharedScripts = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken);
var externalSystems = await _externalSystemRepo.GetAllExternalSystemsAsync(cancellationToken);
var dbConnections = await _externalSystemRepo.GetAllDatabaseConnectionsAsync(cancellationToken);
var notificationLists = await _notificationRepo.GetAllNotificationListsAsync(cancellationToken);
var smtpConfigurations = await _notificationRepo.GetAllSmtpConfigurationsAsync(cancellationToken);
// Map shared scripts
var scriptArtifacts = sharedScripts.Select(s =>
@@ -177,35 +199,23 @@ public class ArtifactDeploymentService
var dbConnectionArtifacts = dbConnections.Select(d =>
new DatabaseConnectionArtifact(d.Name, d.ConnectionString, d.MaxRetries, d.RetryDelay)).ToList();
// Map notification lists
var notificationListArtifacts = notificationLists.Select(nl =>
new NotificationListArtifact(nl.Name, nl.Recipients.Where(r => r.EmailAddress is not null).Select(r => r.EmailAddress!).ToList())).ToList();
// Map SMTP configurations — use Host as the artifact name (matches SQLite PK on site)
var smtpArtifacts = smtpConfigurations.Select(smtp =>
new SmtpConfigurationArtifact(
$"{smtp.Host}:{smtp.Port}", smtp.Host, smtp.Port, smtp.AuthType, smtp.FromAddress,
smtp.Credentials, null, smtp.TlsMode)).ToList();
return new GlobalArtifactSnapshot(
scriptArtifacts,
externalSystemArtifacts,
dbConnectionArtifacts,
notificationListArtifacts,
smtpArtifacts);
dbConnectionArtifacts);
}
/// <summary>
/// Bag of the global artifact sets that do not vary per site, captured once at
/// the start of <see cref="DeployToAllSitesAsync"/> and reused for every per-site
/// command build (DeploymentManager-023).
/// command build (DeploymentManager-023). Notification lists and SMTP
/// configurations are deliberately absent — they are central-only and never
/// shipped to sites (DeploymentManager-025).
/// </summary>
private sealed record GlobalArtifactSnapshot(
IReadOnlyList<SharedScriptArtifact> SharedScripts,
IReadOnlyList<ExternalSystemArtifact> ExternalSystems,
IReadOnlyList<DatabaseConnectionArtifact> DatabaseConnections,
IReadOnlyList<NotificationListArtifact> NotificationLists,
IReadOnlyList<SmtpConfigurationArtifact> SmtpConfigurations);
IReadOnlyList<DatabaseConnectionArtifact> DatabaseConnections);
/// <summary>
/// Deploys artifacts to all sites. Builds a per-site command with that site's data connections.
@@ -226,9 +236,10 @@ public class ArtifactDeploymentService
var perSiteResults = new Dictionary<string, SiteArtifactResult>();
// DeploymentManager-023: hoist the system-wide artifact queries (shared scripts,
// external systems + methods, DB connections, notification lists, SMTP configs)
// OUT of the per-site loop so they run ONCE instead of once per site. Only
// data connections legitimately vary per site, so they stay inside the loop.
// external systems + methods, DB connections) OUT of the per-site loop so they
// run ONCE instead of once per site. Only data connections legitimately vary
// per site, so they stay inside the loop. (Notification lists and SMTP config
// are central-only and not fetched at all — DeploymentManager-025.)
var globals = await FetchGlobalArtifactsAsync(cancellationToken);
// Build per-site commands sequentially (DbContext is not thread-safe).