feature: Prometheus Metrics, Supervisor Dashboard, and Promotion Retry Job
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Provides cursor-paginated access to the audit trail of digitization events
|
||||
/// for a given batch. Events are ordered by occurred_at ascending with id
|
||||
/// as a tie-breaker for events at the same timestamp.
|
||||
/// </summary>
|
||||
public class BatchEventService : IBatchEventService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private const int MaxPageSize = 200;
|
||||
private const int DefaultPageSize = 50;
|
||||
|
||||
public BatchEventService(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<CursorPagedResult<BatchEventResponse>> GetEventsAsync(
|
||||
Guid batchId, DateTimeOffset? after, int pageSize)
|
||||
{
|
||||
// Validate batch exists
|
||||
var batchExists = await _db.DigitizationBatches
|
||||
.AnyAsync(b => b.Id == batchId);
|
||||
|
||||
if (!batchExists)
|
||||
throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");
|
||||
|
||||
// Clamp page size
|
||||
pageSize = Math.Clamp(pageSize, 1, MaxPageSize);
|
||||
|
||||
// Build query
|
||||
var query = _db.DigitizationEvents
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Actor)
|
||||
.Where(e => e.BatchId == batchId);
|
||||
|
||||
// Apply cursor filter — only events strictly after the cursor timestamp
|
||||
if (after.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.OccurredAt > after.Value);
|
||||
}
|
||||
|
||||
// Fetch pageSize+1 rows to determine HasMore without a COUNT query
|
||||
var events = await query
|
||||
.OrderBy(e => e.OccurredAt)
|
||||
.ThenBy(e => e.Id) // tie-breaker for events at the same timestamp
|
||||
.Take(pageSize + 1)
|
||||
.Select(e => new BatchEventResponse(
|
||||
e.Id,
|
||||
e.BatchId,
|
||||
e.EventType.ToDbString(),
|
||||
e.ActorUserId,
|
||||
e.Actor.Username,
|
||||
e.Actor.FullName,
|
||||
e.OccurredAt,
|
||||
e.MetadataJson))
|
||||
.ToListAsync();
|
||||
|
||||
var hasMore = events.Count > pageSize;
|
||||
var page = hasMore ? events.Take(pageSize).ToList() : events;
|
||||
|
||||
// Build next cursor from the last item's OccurredAt
|
||||
string? nextCursor = null;
|
||||
if (hasMore && page.Count > 0)
|
||||
{
|
||||
var lastEvent = page[^1];
|
||||
// ISO-8601 round-trip format preserves full precision
|
||||
nextCursor = lastEvent.OccurredAt.ToString("o");
|
||||
}
|
||||
|
||||
return new CursorPagedResult<BatchEventResponse>(
|
||||
Items: page,
|
||||
PageSize: pageSize,
|
||||
NextCursor: nextCursor,
|
||||
HasMore: hasMore);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user