feature: Self-Service Clinical Testing Sessions
CI / backend (push) Successful in 8m52s
CI / frontend (push) Failing after 1m39s

This commit is contained in:
voltsrage
2026-08-06 04:03:04 +08:00
parent 1d28880920
commit 80b009fd23
41 changed files with 4922 additions and 123 deletions
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
[ApiController]
[Produces("application/json")]
@@ -7,9 +8,15 @@ using Microsoft.AspNetCore.Mvc;
public class AlertQualityMetricsController : ControllerBase
{
private readonly IAlertQualityMetricsService _metrics;
private readonly SimulationOptions _simulation;
public AlertQualityMetricsController(IAlertQualityMetricsService metrics) =>
public AlertQualityMetricsController(
IAlertQualityMetricsService metrics,
IOptions<SimulationOptions> simulation)
{
_metrics = metrics;
_simulation = simulation.Value;
}
/// <summary>
/// Returns alert quality metric snapshots for a time range, optionally filtered by alert type.
@@ -70,4 +77,67 @@ public class AlertQualityMetricsController : ControllerBase
var summary = await _metrics.GetSummaryAsync(periodStart, periodEnd);
return Ok(ApiResponse<AlertQualitySummaryResponse>.Ok(summary));
}
}
/// <summary>
/// Per-alert feedback rows with optional scenario attribution (when simulation is enabled).
/// Filter with <c>?scenarioId=</c> to limit to one simulated scenario.
/// </summary>
[HttpGet("api/v1/alerts/quality-metrics/feedback")]
[AuthorizePermission(ClinicalPermissions.AnalyticsRead)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ListFeedback(
[FromQuery] string? scenarioId,
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to)
{
var periodEnd = to ?? DateTimeOffset.UtcNow;
var periodStart = from ?? periodEnd.AddDays(-7);
if (periodStart >= periodEnd)
{
return BadRequest(ApiResponse<object>.Fail(
400, "'from' must be before 'to'.", "INVALID_DATE_RANGE"));
}
// Scenario filter only applies when simulation attribution is available.
var filterScenario = _simulation.Enabled ? scenarioId : null;
var rows = await _metrics.ListFeedbackAsync(periodStart, periodEnd, filterScenario);
if (_simulation.Enabled)
{
var withAttribution = rows.Select(r => new
{
id = r.Id,
alertId = r.AlertId,
alertType = r.AlertType,
feedbackType = r.FeedbackType,
comment = r.Comment,
createdAt = r.CreatedAt,
scenarioId = r.ScenarioId,
sessionId = r.SessionId,
});
return Ok(ApiResponse<object>.Ok(new
{
periodStart,
periodEnd,
items = withAttribution,
}));
}
var withoutAttribution = rows.Select(r => new
{
id = r.Id,
alertId = r.AlertId,
alertType = r.AlertType,
feedbackType = r.FeedbackType,
comment = r.Comment,
createdAt = r.CreatedAt,
});
return Ok(ApiResponse<object>.Ok(new
{
periodStart,
periodEnd,
items = withoutAttribution,
}));
}
}