Run initial test for Climate Resilience Verification Suite
Add first part of Alert Quality Analytics
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class AlertQualityMetricsService : IAlertQualityMetricsService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public AlertQualityMetricsService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<AlertQualityMetricResponse>> ListAsync(
|
||||
AlertType? alertType, DateTimeOffset from, DateTimeOffset to)
|
||||
{
|
||||
var query = _db.AlertQualityMetrics
|
||||
.AsNoTracking()
|
||||
.Where(m => m.WindowStart >= from && m.WindowEnd <= to);
|
||||
|
||||
if (alertType.HasValue)
|
||||
query = query.Where(m => m.AlertType == alertType.Value);
|
||||
|
||||
var rows = await query
|
||||
.OrderByDescending(m => m.WindowStart)
|
||||
.ThenBy(m => m.AlertType)
|
||||
.ToListAsync();
|
||||
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<AlertQualitySummaryResponse> GetSummaryAsync(
|
||||
DateTimeOffset from, DateTimeOffset to)
|
||||
{
|
||||
var snapshots = await _db.AlertQualityMetrics
|
||||
.AsNoTracking()
|
||||
.Where(m => m.WindowStart >= from && m.WindowEnd <= to)
|
||||
.ToListAsync();
|
||||
|
||||
if (snapshots.Count == 0)
|
||||
{
|
||||
return new AlertQualitySummaryResponse(
|
||||
from, to, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
var totalAlerts = snapshots.Sum(s => s.TotalAlerts);
|
||||
var totalFeedback = snapshots.Sum(s => s.FeedbackCount);
|
||||
var totalAcknowledged = snapshots.Sum(s => s.AcknowledgedCount);
|
||||
var totalUseful = snapshots.Sum(s => s.FeedbackUsefulCount);
|
||||
var totalFalsePositive = snapshots.Sum(s => s.FeedbackFalsePositiveCount);
|
||||
var totalWouldAct = snapshots.Sum(s => s.FeedbackWouldActCount);
|
||||
|
||||
var weightedAckSeconds = snapshots.Sum(s => s.AvgSecondsToAcknowledge * s.TotalAlerts);
|
||||
var weightedResolveSeconds = snapshots.Sum(s => s.AvgSecondsToResolution * s.ResolvedCount);
|
||||
|
||||
return new AlertQualitySummaryResponse(
|
||||
PeriodStart: from,
|
||||
PeriodEnd: to,
|
||||
TotalAlerts: totalAlerts,
|
||||
TotalFeedback: totalFeedback,
|
||||
AcknowledgementRate: totalAlerts == 0 ? 0 : (double)totalAcknowledged / totalAlerts,
|
||||
FalsePositiveRate: totalFeedback == 0 ? 0 : (double)totalFalsePositive / totalFeedback,
|
||||
UsefulRate: totalFeedback == 0 ? 0 : (double)totalUseful / totalFeedback,
|
||||
WouldActRate: totalFeedback == 0 ? 0 : (double)totalWouldAct / totalFeedback,
|
||||
AvgSecondsToAcknowledge: totalAlerts == 0 ? 0 : weightedAckSeconds / totalAlerts,
|
||||
AvgSecondsToResolution: snapshots.Sum(s => s.ResolvedCount) == 0
|
||||
? 0
|
||||
: weightedResolveSeconds / snapshots.Sum(s => s.ResolvedCount));
|
||||
}
|
||||
|
||||
private static AlertQualityMetricResponse Map(AlertQualityMetric m) =>
|
||||
new(
|
||||
m.Id,
|
||||
m.AlertType.ToString(),
|
||||
m.WindowStart,
|
||||
m.WindowEnd,
|
||||
m.TotalAlerts,
|
||||
m.AcknowledgedCount,
|
||||
m.ResolvedCount,
|
||||
m.EscalatedCount,
|
||||
m.FeedbackUsefulCount,
|
||||
m.FeedbackFalsePositiveCount,
|
||||
m.FeedbackWouldActCount,
|
||||
m.FeedbackCount,
|
||||
m.AcknowledgementRate,
|
||||
m.FalsePositiveRate,
|
||||
m.UsefulRate,
|
||||
m.WouldActRate,
|
||||
m.AvgSecondsToAcknowledge,
|
||||
m.AvgSecondsToResolution,
|
||||
m.ComputedAt);
|
||||
}
|
||||
@@ -163,6 +163,54 @@ public class AlertService : IAlertService
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<AlertFeedback> SubmitFeedbackAsync(
|
||||
Guid alertId, AlertFeedbackType type, string? comment)
|
||||
{
|
||||
if (!_currentUser.IsAuthenticated || _currentUser.UserId is null)
|
||||
throw new ValidationException("Authentication required.", "AUTH_REQUIRED");
|
||||
|
||||
var userId = _currentUser.UserId.Value;
|
||||
|
||||
var alert = await _db.ClinicalAlerts.FindAsync(alertId);
|
||||
if (alert is null)
|
||||
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
|
||||
|
||||
if (alert.Status == AlertStatus.Open)
|
||||
throw new ValidationException(
|
||||
"Feedback can only be submitted on acknowledged or resolved alerts.",
|
||||
"ALERT_NOT_REVIEWABLE");
|
||||
|
||||
var alreadySubmitted = await _db.AlertFeedbacks
|
||||
.AnyAsync(f => f.AlertId == alertId && f.UserId == userId);
|
||||
if (alreadySubmitted)
|
||||
throw new ConflictException(
|
||||
"You have already submitted feedback for this alert.",
|
||||
"FEEDBACK_ALREADY_SUBMITTED");
|
||||
|
||||
var feedback = new AlertFeedback
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AlertId = alertId,
|
||||
UserId = userId,
|
||||
FeedbackType = type,
|
||||
Comment = string.IsNullOrWhiteSpace(comment) ? null : comment.Trim(),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_db.AlertFeedbacks.Add(feedback);
|
||||
alert.FeedbackReceived = true;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _audit.WriteAsync(
|
||||
AuditAction.AlertFeedbackSubmitted,
|
||||
"ClinicalAlert",
|
||||
alert.Id,
|
||||
newValue: new { feedbackType = type.ToDbString(), feedback.UserId },
|
||||
reason: comment);
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
private async Task<int> ResolveSuppressionWindowMinutesAsync(
|
||||
AlertType alertType, int defaultWindowMinutes)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
public interface IAlertQualityMetricsService
|
||||
{
|
||||
Task<IReadOnlyList<AlertQualityMetricResponse>> ListAsync(
|
||||
AlertType? alertType, DateTimeOffset from, DateTimeOffset to);
|
||||
|
||||
Task<AlertQualitySummaryResponse> GetSummaryAsync(
|
||||
DateTimeOffset from, DateTimeOffset to);
|
||||
}
|
||||
@@ -13,4 +13,5 @@ public interface IAlertService
|
||||
Task<ClinicalAlert> ResolveAsync(Guid id);
|
||||
Task ApplySyncedAcknowledgmentAsync(Guid alertId, SyncedAlertAcknowledgment ack, CancellationToken ct);
|
||||
Task ApplySyncedResolutionAsync(Guid alertId, SyncedAlertResolution resolve, CancellationToken ct);
|
||||
Task<AlertFeedback> SubmitFeedbackAsync(Guid alertId, AlertFeedbackType type, string? comment);
|
||||
}
|
||||
Reference in New Issue
Block a user