feat(ops): pipeline-health dashboard

- Add a /health ops page: snapshot freshness (last-capture-at, colour-coded), 24h vs
  total snapshots + anomalies, events tracked, sports covered, and the four worker
  on/off states. Nav entry under System; localized en+ru.
- New IPipelineHealthService + ISnapshotRepository.GetLatestCapturedAtAsync (max
  CapturedAt via indexed ORDER BY/LIMIT 1), with a real-SQLite round-trip test.
This commit is contained in:
2026-05-29 01:32:41 +03:00
parent b67030ae7f
commit 5eb3dec24b
10 changed files with 270 additions and 0 deletions
@@ -366,6 +366,29 @@ public sealed class RoundTripTests : IDisposable
many.Keys.Select(k => k.Value).Should().BeEquivalentTo(new[] { "Q1", "Q3" });
}
[Fact]
public async Task Snapshot_GetLatestCapturedAt_ReturnsMostRecentOrNull()
{
// Empty store → null.
(await _snapshotRepo.GetLatestCapturedAtAsync()).Should().BeNull();
await _eventRepo.AddAsync(BuildEvent("L100"));
await _eventRepo.SaveChangesAsync();
var older = new DateTimeOffset(2026, 5, 1, 12, 0, 0, MoscowOffset);
var newer = new DateTimeOffset(2026, 5, 9, 18, 30, 0, MoscowOffset);
OddsSnapshot Snap(DateTimeOffset at) =>
new(new EventId("L100"), at, OddsSource.Live,
new List<Bet> { new(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(1.90m)) });
await _snapshotRepo.AddAsync(Snap(older));
await _snapshotRepo.AddAsync(Snap(newer));
await _snapshotRepo.SaveChangesAsync();
_fixture.DbContext.ChangeTracker.Clear();
(await _snapshotRepo.GetLatestCapturedAtAsync()).Should().Be(newer);
}
// ── Helpers ─────────────────────────────────────────────────────────────
private static Event BuildEvent(string id, DateTimeOffset? scheduledAt = null) =>