feat(sitecallaudit): PullSiteCalls reconciliation plumbing (store read + RPC + site handler + central client)

Site Call Audit (#22): build the documented periodic reconciliation PULL
self-heal path for the eventually-consistent central SiteCalls mirror, as a
dedicated PullSiteCalls gRPC RPC kept separate from the audit pull. This is the
pull PLUMBING only; the central reconciliation tick is a separate follow-up.

- IOperationTrackingStore.ReadChangedSinceAsync(sinceUtc, batchSize): inclusive
  UpdatedAtUtc cursor, oldest-first, batch-capped; SQLite impl projects tracking
  rows onto SiteCallOperational (Kind->Channel, TargetSummary->Target, SourceSite
  left empty - the store has no site-id column).
- sitestream.proto: rpc PullSiteCalls + PullSiteCallsRequest/Response, mirroring
  PullAuditEvents; regenerated checked-in SiteStreamGrpc/*.cs.
- SiteCallDtoMapper.ToDto(SiteCallOperational): inverse of FromDto for the handler.
- SiteStreamGrpcServer.PullSiteCalls handler + SetOperationTrackingStore seam;
  Host wires the seam alongside SetSiteAuditQueue (site roles only).
- Central IPullSiteCallsClient + GrpcPullSiteCallsClient (home: AuditLog/Central to
  reuse ISiteEnumerator; SiteCallAudit does not reference AuditLog). Re-stamps
  SourceSite from the dialed siteId; no-throw on tolerable transport faults;
  SpecifyKind (not ToUniversalTime) cursor handling. Central-only DI registration.

Tests: ReadChangedSinceAsync (4), PullSiteCalls handler (6), GrpcPullSiteCallsClient
(8). Full solution build 0 warnings/0 errors (TreatWarningsAsErrors).
This commit is contained in:
Joseph Doherty
2026-06-15 10:39:06 -04:00
parent c092e89fd1
commit 963e3427da
15 changed files with 1751 additions and 23 deletions
@@ -118,4 +118,40 @@ public interface IOperationTrackingStore
Task PurgeTerminalAsync(
DateTime olderThanUtc,
CancellationToken ct = default);
/// <summary>
/// Reconciliation read (Site Call Audit #22): return tracking rows whose
/// <c>UpdatedAtUtc</c> is at or after <paramref name="sinceUtc"/> as
/// <see cref="SiteCallOperational"/> projections, ordered by
/// <c>UpdatedAtUtc</c> ascending and capped at <paramref name="batchSize"/>.
/// This is the site-side feed for central's <c>PullSiteCalls</c> RPC — the
/// documented periodic self-heal pull that backfills the eventually-consistent
/// central <c>SiteCalls</c> mirror when best-effort push telemetry is lost.
/// </summary>
/// <remarks>
/// <para>
/// The lower bound is inclusive so a caller can resume from the last
/// returned <c>UpdatedAtUtc</c> without skipping a row that shares that
/// instant; central ingest is insert-if-not-exists then upsert-on-newer, so
/// re-reading the boundary row is a harmless no-op. The oldest-first cap lets
/// the caller advance the cursor monotonically across follow-up pulls.
/// </para>
/// <para>
/// <see cref="SiteCallOperational.SourceSite"/> is left as the empty string:
/// the site id is not a tracking-store column, and the central client re-stamps
/// it from the <c>siteId</c> it dialed (the only authority that knows which
/// site the rows came from). <see cref="SiteCallOperational.Channel"/> is
/// projected from the row's <c>Kind</c> (<c>DbWriteCached → DbOutbound</c>,
/// otherwise <c>ApiOutbound</c>) and <see cref="SiteCallOperational.Target"/>
/// from <c>TargetSummary</c>.
/// </para>
/// </remarks>
/// <param name="sinceUtc">Inclusive lower bound on <c>UpdatedAtUtc</c>; <see cref="DateTime.MinValue"/> reads from the start.</param>
/// <param name="batchSize">Maximum number of rows to return (oldest first).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The matching rows projected to <see cref="SiteCallOperational"/>, oldest-first, capped at <paramref name="batchSize"/>.</returns>
Task<IReadOnlyList<SiteCallOperational>> ReadChangedSinceAsync(
DateTime sinceUtc,
int batchSize,
CancellationToken ct = default);
}
@@ -0,0 +1,17 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
/// <summary>
/// Site Call Audit (#22) periodic reconciliation pull response: the next batch of
/// site cached-call operational rows (the eventually-consistent <c>SiteCalls</c>
/// mirror's self-heal feed) plus a <paramref name="MoreAvailable"/> flag signalling
/// the caller to advance the watermark and pull again. Mirrors
/// <see cref="PullAuditEventsResponse"/>; carries the central <see cref="SiteCall"/>
/// entity the ingest path upserts. See Component-SiteCallAudit.md.
/// </summary>
/// <param name="SiteCalls">The next batch of operational rows, ordered oldest-first by <see cref="SiteCall.UpdatedAtUtc"/>.</param>
/// <param name="MoreAvailable">True when the site saturated the requested batch size — the caller should advance the cursor and pull again.</param>
public sealed record PullSiteCallsResponse(
IReadOnlyList<SiteCall> SiteCalls,
bool MoreAvailable);