Merge branch 'fix/standby-secret-mirror-status' (M1: standby mirror failures on the health report)

# Conflicts:
#	CLAUDE.md
#	docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md
This commit is contained in:
Joseph Doherty
2026-09-28 21:37:03 -04:00
29 changed files with 1738 additions and 106 deletions
@@ -0,0 +1,26 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
/// <summary>
/// One site node's latest deployed-secrets mirror outcome, as carried on
/// <see cref="SiteHealthReport.SecretMirrorStatuses"/> (review finding M1).
/// </summary>
/// <remarks>
/// Every site node runs its own <c>DeployedSecretsMirror</c> and records its current
/// failure set in the LocalDb-replicated <c>secret_mirror_status</c> table, keyed by node
/// name; the ACTIVE node reads every row and ships them on its health report, so a
/// STANDBY node's failures (most importantly a master-key mismatch that stops a deployed
/// secret decrypting there) reach central before a failover exposes them. Carries names
/// and human-readable messages only — never a secret value or ciphertext.
/// </remarks>
/// <param name="NodeName">The reporting node's semantic name (<c>node-a</c>/<c>node-b</c>), or its host name when unconfigured.</param>
/// <param name="CheckedAtUtc">When that node last recorded a CHANGE in its failure set (or its first pass after start-up) — not the time of its latest pass.</param>
/// <param name="Failures">That node's current mirror failures; empty when the node's mirror is healthy.</param>
public sealed record SecretMirrorNodeStatus(
string NodeName,
DateTimeOffset CheckedAtUtc,
IReadOnlyList<SecretMirrorFailure> Failures);
/// <summary>A single deployed secret that failed to apply and/or decrypt on one site node.</summary>
/// <param name="SecretName">The secret's name.</param>
/// <param name="Message">A human-readable failure message — never includes the secret's value.</param>
public sealed record SecretMirrorFailure(string SecretName, string Message);
@@ -173,6 +173,20 @@ public record SiteHealthReport(
/// </para>
/// </remarks>
public bool? StartupReconcilerActive { get; init; }
/// <summary>
/// Every site node's latest deployed-secrets mirror outcome (review finding M1) — one
/// entry per node that has recorded a row in the replicated <c>secret_mirror_status</c>
/// table, including the STANDBY node, whose own health report is never sent.
/// </summary>
/// <remarks>
/// <b>Null = no data</b>: a central self-report, a pre-M1 site build, a site whose
/// reporter has not run yet, or a failed read. An EMPTY list means the feature is wired
/// but no node has recorded a status yet. A node entry with an empty
/// <see cref="SecretMirrorNodeStatus.Failures"/> list is a healthy mirror. Names and
/// messages only — never secret values or ciphertext.
/// </remarks>
public IReadOnlyList<SecretMirrorNodeStatus>? SecretMirrorStatuses { get; init; }
}
/// <summary>