fix(high-severity): close 9 of 10 open High findings across 8 modules

Comm-016: delete dead HandleConnectionStateChanged + _debugSubscriptions /
_inProgressDeployments tracking + ConnectionStateChanged message record.
Disconnect detection is owned by the transport layers (gRPC keepalive PING
~25s; Ask-timeout at CommunicationService). Updates the
Component-Communication.md design doc to make that explicit.

SnF-018: NotificationForwarder.DeliverAsync now discards a corrupt buffered
payload (Warning log + return true) instead of returning false and parking
the row — honoring the design's "notifications do not park" invariant.

DM-018: reconciliation no longer force-sets Enabled, preserving an
intentional Disabled state after central failover.

ESG-018: DeliverBufferedAsync (both ExternalSystemClient + DatabaseGateway)
catches JsonException and returns false, turning a corrupt buffered row
into a parked operation instead of a retry-forever poison message.

InboundAPI-022: register ActiveNodeGate as IActiveNodeGate in the Central
DI branch so standby-node gating is actually wired up in production.

NS-019: remove orphaned NotificationDeliveryService /
INotificationDeliveryService / NotificationResult; central notification
delivery now lives entirely in NotificationOutbox.

SEL-016: normalise From/To filters to UTC before ISO-string compare so
non-UTC DateTimeOffset clients no longer get spuriously excluded events.

TE-017: include Description on attributes/alarms and a HashableConnections
projection (protocol, endpoint JSON, failover count) in the revision hash
and DiffService; staleness detection now catches description-only and
connection-endpoint edits.

Transport-001 and Transport-002 (also High) remain Open — they're being
handled in a follow-up batch because both touch BundleImporter.cs and
must serialise.
This commit is contained in:
Joseph Doherty
2026-05-28 05:40:15 -04:00
parent f936f55f51
commit ac96b83b08
38 changed files with 852 additions and 1729 deletions
@@ -111,12 +111,14 @@ public class DiffService
a.CanonicalName == b.CanonicalName &&
a.Value == b.Value &&
a.DataType == b.DataType &&
a.Description == b.Description &&
a.IsLocked == b.IsLocked &&
a.DataSourceReference == b.DataSourceReference &&
a.BoundDataConnectionId == b.BoundDataConnectionId;
private static bool AlarmsEqual(ResolvedAlarm a, ResolvedAlarm b) =>
a.CanonicalName == b.CanonicalName &&
a.Description == b.Description &&
a.PriorityLevel == b.PriorityLevel &&
a.IsLocked == b.IsLocked &&
a.TriggerType == b.TriggerType &&
@@ -132,4 +134,27 @@ public class DiffService
a.ParameterDefinitions == b.ParameterDefinitions &&
a.ReturnDefinition == b.ReturnDefinition &&
a.MinTimeBetweenRuns == b.MinTimeBetweenRuns;
/// <summary>
/// Compares two <see cref="ConnectionConfig"/> instances for equality across
/// the fields that travel in the deployment package: protocol, primary and
/// backup configuration JSON, and failover retry count. Used by callers that
/// need to detect connection-endpoint drift; the public diff shape only
/// exposes attribute / alarm / script changes today (see TemplateEngine-018
/// for the diff-shape extension that surfaces added / removed / changed
/// connections in the UI).
/// </summary>
/// <param name="a">First connection configuration.</param>
/// <param name="b">Second connection configuration.</param>
/// <returns>True when both configurations are equal.</returns>
public static bool ConnectionsEqual(ConnectionConfig a, ConnectionConfig b)
{
ArgumentNullException.ThrowIfNull(a);
ArgumentNullException.ThrowIfNull(b);
return a.Protocol == b.Protocol &&
a.ConfigurationJson == b.ConfigurationJson &&
a.BackupConfigurationJson == b.BackupConfigurationJson &&
a.FailoverRetryCount == b.FailoverRetryCount;
}
}
@@ -52,6 +52,7 @@ public class RevisionHashService
CanonicalName = a.CanonicalName,
Value = a.Value,
DataType = a.DataType,
Description = a.Description,
IsLocked = a.IsLocked,
DataSourceReference = a.DataSourceReference,
BoundDataConnectionId = a.BoundDataConnectionId
@@ -62,6 +63,7 @@ public class RevisionHashService
.Select(a => new HashableAlarm
{
CanonicalName = a.CanonicalName,
Description = a.Description,
PriorityLevel = a.PriorityLevel,
IsLocked = a.IsLocked,
TriggerType = a.TriggerType,
@@ -82,7 +84,20 @@ public class RevisionHashService
ReturnDefinition = s.ReturnDefinition,
MinTimeBetweenRunsTicks = s.MinTimeBetweenRuns?.Ticks
})
.ToList()
.ToList(),
Connections = configuration.Connections is { Count: > 0 }
? new SortedDictionary<string, HashableConnection>(
configuration.Connections.ToDictionary(
kvp => kvp.Key,
kvp => new HashableConnection
{
BackupConfigurationJson = kvp.Value.BackupConfigurationJson,
ConfigurationJson = kvp.Value.ConfigurationJson,
FailoverRetryCount = kvp.Value.FailoverRetryCount,
Protocol = kvp.Value.Protocol
}),
StringComparer.Ordinal)
: null
};
var json = JsonSerializer.Serialize(hashInput, CanonicalJsonOptions);
@@ -108,6 +123,12 @@ public class RevisionHashService
/// </summary>
public List<HashableAttribute> Attributes { get; init; } = [];
/// <summary>
/// Data connection configurations keyed by connection name. Sorted by key
/// (ordinal) to keep serialization deterministic. Null when the deployment
/// package carries no connections.
/// </summary>
public SortedDictionary<string, HashableConnection>? Connections { get; init; }
/// <summary>
/// The unique instance name.
/// </summary>
public string InstanceUniqueName { get; init; } = string.Empty;
@@ -144,6 +165,11 @@ public class RevisionHashService
/// </summary>
public string DataType { get; init; } = string.Empty;
/// <summary>
/// The attribute description (authoring-time documentation that still
/// travels with the deployed payload).
/// </summary>
public string? Description { get; init; }
/// <summary>
/// Whether the attribute is locked.
/// </summary>
public bool IsLocked { get; init; }
@@ -160,6 +186,11 @@ public class RevisionHashService
/// </summary>
public string CanonicalName { get; init; } = string.Empty;
/// <summary>
/// The alarm description (authoring-time documentation that still
/// travels with the deployed payload).
/// </summary>
public string? Description { get; init; }
/// <summary>
/// Whether the alarm is locked.
/// </summary>
public bool IsLocked { get; init; }
@@ -181,6 +212,26 @@ public class RevisionHashService
public string TriggerType { get; init; } = string.Empty;
}
private sealed record HashableConnection
{
/// <summary>
/// Backup connection configuration JSON, if any.
/// </summary>
public string? BackupConfigurationJson { get; init; }
/// <summary>
/// Primary connection configuration JSON.
/// </summary>
public string? ConfigurationJson { get; init; }
/// <summary>
/// Number of failover retries before giving up.
/// </summary>
public int FailoverRetryCount { get; init; }
/// <summary>
/// Protocol name (e.g. "OpcUa").
/// </summary>
public string Protocol { get; init; } = string.Empty;
}
private sealed record HashableScript
{
/// <summary>