feature: Prometheus Metrics, Supervisor Dashboard, and Promotion Retry Job

This commit is contained in:
voltsrage
2026-06-27 13:29:04 +08:00
parent e22d33b654
commit 04bdb7e85c
31 changed files with 4482 additions and 259 deletions
@@ -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);
}
}