feature: Degraded Operations Visibility
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
|
||||
public class OperationsService : IOperationsService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IClinicalSyncService _sync;
|
||||
|
||||
public OperationsService(AppDbContext db, IClinicalSyncService sync)
|
||||
{
|
||||
_db = db;
|
||||
_sync = sync;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<GatewayFleetItem>> GetGatewayFleetAsync(
|
||||
GatewayFleetFilter filter, CancellationToken ct)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(_db.Database.GetConnectionString());
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
g.id,
|
||||
g.gateway_code AS GatewayCode,
|
||||
g.department AS Department,
|
||||
s.site_code AS SiteCode,
|
||||
s.name AS SiteName,
|
||||
g.status AS Status,
|
||||
g.reported_buffer_depth AS ReportedBufferDepth,
|
||||
g.last_heartbeat_at AS LastHeartbeatAt,
|
||||
g.last_sync_at AS LastSyncAt,
|
||||
EXTRACT(EPOCH FROM (NOW() - g.last_heartbeat_at)) / 60 AS MinutesSinceHeartbeat
|
||||
FROM ward_gateways g
|
||||
JOIN clinical_sites s ON s.id = g.site_id
|
||||
WHERE (@Status IS NULL OR g.status = @Status)
|
||||
AND (@SiteId IS NULL OR g.site_id = @SiteId)
|
||||
ORDER BY g.status DESC, MinutesSinceHeartbeat DESC NULLS LAST
|
||||
""";
|
||||
|
||||
var rows = await conn.QueryAsync<GatewayFleetItem>(sql, new
|
||||
{
|
||||
Status = filter.Status,
|
||||
SiteId = filter.SiteId
|
||||
});
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<GatewayDetailResponse> GetGatewayDetailAsync(Guid gatewayId, CancellationToken ct)
|
||||
{
|
||||
var fleet = await GetGatewayFleetAsync(new GatewayFleetFilter(null, null), ct);
|
||||
var gateway = fleet.FirstOrDefault(g => g.Id == gatewayId)
|
||||
?? throw new NotFoundException("Gateway not found.", "GATEWAY_NOT_FOUND");
|
||||
|
||||
var siteId = await _db.WardGateways
|
||||
.Where(g => g.Id == gatewayId)
|
||||
.Select(g => g.SiteId)
|
||||
.FirstAsync(ct);
|
||||
|
||||
var history = await _sync.GetSyncHistoryAsync(siteId, gatewayId, 10, ct);
|
||||
|
||||
var lastConflictBatch = await _db.ClinicalSyncBatches
|
||||
.AsNoTracking()
|
||||
.Where(b => b.GatewayId == gatewayId && b.Status == ClinicalSyncBatchStatus.Conflict)
|
||||
.OrderByDescending(b => b.SubmittedAt)
|
||||
.Include(b => b.Conflicts)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
var pendingConflicts = lastConflictBatch?.Conflicts.Count ?? 0;
|
||||
|
||||
return new GatewayDetailResponse(gateway, history, pendingConflicts);
|
||||
}
|
||||
|
||||
public async Task<SiteGatewaySummaryResponse> GetSiteSummaryAsync(Guid siteId, CancellationToken ct)
|
||||
{
|
||||
var siteExists = await _db.ClinicalSites.AnyAsync(s => s.Id == siteId, ct);
|
||||
if (!siteExists)
|
||||
throw new NotFoundException("Site not found.", "SITE_NOT_FOUND");
|
||||
|
||||
await using var conn = new NpgsqlConnection(_db.Database.GetConnectionString());
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
SELECT
|
||||
COUNT(*) AS TotalGateways,
|
||||
COUNT(*) FILTER (WHERE status = 'ONLINE') AS Online,
|
||||
COUNT(*) FILTER (WHERE status = 'DEGRADED') AS Degraded,
|
||||
COUNT(*) FILTER (WHERE status = 'OFFLINE') AS Offline,
|
||||
COALESCE(SUM(reported_buffer_depth), 0) AS TotalBufferedEvents
|
||||
FROM ward_gateways
|
||||
WHERE site_id = @SiteId
|
||||
""";
|
||||
|
||||
var row = await conn.QuerySingleAsync(sql, new { SiteId = siteId });
|
||||
|
||||
return new SiteGatewaySummaryResponse(
|
||||
siteId,
|
||||
(int)row.totalgateways,
|
||||
(int)row.online,
|
||||
(int)row.degraded,
|
||||
(int)row.offline,
|
||||
(int)row.totalbufferedevents);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user