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
File diff suppressed because it is too large Load Diff
@@ -559,6 +559,32 @@ public static class CentralControlDtoMapper
dto.SiteAuditBacklog = snapshot;
}
// Review finding M1: wrapper presence keeps null ("no data") distinct from empty.
if (msg.SecretMirrorStatuses is { } mirrorStatuses)
{
var wrapper = new SecretMirrorStatusListDto();
foreach (var node in mirrorStatuses)
{
var nodeDto = new SecretMirrorNodeStatusDto
{
NodeName = node.NodeName,
CheckedAtUtc = Timestamp.FromDateTimeOffset(node.CheckedAtUtc),
};
foreach (var failure in node.Failures)
{
nodeDto.Failures.Add(new SecretMirrorFailureDto
{
SecretName = failure.SecretName,
Message = failure.Message,
});
}
wrapper.Nodes.Add(nodeDto);
}
dto.SecretMirrorStatuses = wrapper;
}
return dto;
}
@@ -620,6 +646,19 @@ public static class CentralControlDtoMapper
OnDiskBytes: snapshot.OnDiskBytes);
}
List<SecretMirrorNodeStatus>? mirrorStatuses = null;
if (dto.SecretMirrorStatuses is { } mirrorWrapper)
{
mirrorStatuses = new List<SecretMirrorNodeStatus>(mirrorWrapper.Nodes.Count);
foreach (var node in mirrorWrapper.Nodes)
{
mirrorStatuses.Add(new SecretMirrorNodeStatus(
node.NodeName,
node.CheckedAtUtc?.ToDateTimeOffset() ?? DateTimeOffset.MinValue,
node.Failures.Select(f => new SecretMirrorFailure(f.SecretName, f.Message)).ToList()));
}
}
return new SiteHealthReport(
SiteId: dto.SiteId,
SequenceNumber: dto.SequenceNumber,
@@ -657,6 +696,7 @@ public static class CentralControlDtoMapper
DetachedScriptThreads = dto.DetachedScriptThreads,
ScriptRunShedCount = dto.ScriptRunShedCount,
SiteStreamAlarmDropCount = dto.SiteStreamAlarmDropCount,
SecretMirrorStatuses = mirrorStatuses,
};
}
@@ -193,6 +193,25 @@ message NodeStatusListDto {
repeated NodeStatusDto nodes = 1;
}
// Review finding M1: one site node's deployed-secrets mirror failures. Names and
// human-readable messages only — never a secret value or ciphertext.
message SecretMirrorFailureDto {
string secret_name = 1;
string message = 2;
}
message SecretMirrorNodeStatusDto {
string node_name = 1;
google.protobuf.Timestamp checked_at_utc = 2;
repeated SecretMirrorFailureDto failures = 3;
}
// Presence wrapper (same reason as NodeStatusListDto): absent = null ("no data"),
// present-but-empty = the feature is wired and no node has recorded a status yet.
message SecretMirrorStatusListDto {
repeated SecretMirrorNodeStatusDto nodes = 1;
}
// Site -> Central: periodic site health report. Mirrors Commons SiteHealthReport.
// Additive-only evolution: field numbers are never reused.
message SiteHealthReportDto {
@@ -245,6 +264,10 @@ message SiteHealthReportDto {
// dedicated alarm publish queue because it was at capacity. Point-in-time
// (not reset on collect), like site_event_log_write_failures.
int64 site_stream_alarm_drop_count = 32;
// Review finding M1: every site node's deployed-secrets mirror status (the standby's
// included — it is read from a LocalDb-replicated table on the active node).
// Absent means null ("no data": central self-report, pre-M1 site, or not read yet).
SecretMirrorStatusListDto secret_mirror_statuses = 33;
}
// Central -> Site: health report ack, so a lost report is observable.