Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -896,6 +896,146 @@ app.MapGet("/api/v2/notify/deliveries/{deliveryId}", async (
: Results.NotFound(Error("not_found", $"Delivery {deliveryId} not found.", context));
});
// =============================================
// Delivery Retry and Stats (NOTIFY-016)
// =============================================
app.MapPost("/api/v2/notify/deliveries/{deliveryId}/retry", async (
HttpContext context,
string deliveryId,
StellaOps.Notifier.WebService.Contracts.DeliveryRetryRequest? request,
INotifyDeliveryRepository deliveryRepository,
INotifyAuditRepository auditRepository,
TimeProvider timeProvider) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
var actor = context.Request.Headers["X-StellaOps-Actor"].ToString();
if (string.IsNullOrWhiteSpace(actor)) actor = "api";
var delivery = await deliveryRepository.GetAsync(tenantId, deliveryId, context.RequestAborted).ConfigureAwait(false);
if (delivery is null)
{
return Results.NotFound(Error("not_found", $"Delivery {deliveryId} not found.", context));
}
if (delivery.Status == NotifyDeliveryStatus.Sent || delivery.Status == NotifyDeliveryStatus.Delivered)
{
return Results.BadRequest(Error("delivery_already_completed", "Cannot retry a completed delivery.", context));
}
var now = timeProvider.GetUtcNow();
var newAttemptNumber = delivery.Attempts.Length + 1;
// Create new attempt and update delivery to pending status for retry
var newAttempt = new NotifyDeliveryAttempt(now, NotifyDeliveryAttemptStatus.Enqueued);
var updatedDelivery = NotifyDelivery.Create(
delivery.DeliveryId,
delivery.TenantId,
delivery.RuleId,
delivery.ActionId,
delivery.EventId,
delivery.Kind,
NotifyDeliveryStatus.Pending,
"Retry requested",
delivery.Rendered,
delivery.Attempts.Append(newAttempt),
delivery.Metadata,
delivery.CreatedAt);
await deliveryRepository.UpdateAsync(updatedDelivery, context.RequestAborted).ConfigureAwait(false);
// Audit the retry
try
{
await auditRepository.AppendAsync(new NotifyAuditEntryDocument
{
TenantId = tenantId,
Actor = actor,
Action = "delivery.retry",
EntityId = deliveryId,
EntityType = "delivery",
Timestamp = now,
Payload = System.Text.Json.JsonSerializer.SerializeToNode(new { deliveryId, reason = request?.Reason, forceChannel = request?.ForceChannel }) as System.Text.Json.Nodes.JsonObject
}, context.RequestAborted).ConfigureAwait(false);
}
catch { /* Ignore audit failures */ }
return Results.Ok(new
{
deliveryId,
retried = true,
newAttemptNumber,
scheduledAt = now.ToString("O"),
message = "Delivery scheduled for retry"
});
});
app.MapGet("/api/v2/notify/deliveries/stats", async (
HttpContext context,
INotifyDeliveryRepository deliveryRepository) =>
{
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
}
var allDeliveries = await deliveryRepository.ListAsync(tenantId, context.RequestAborted).ConfigureAwait(false);
var sent = allDeliveries.Count(d => d.Status == NotifyDeliveryStatus.Sent || d.Status == NotifyDeliveryStatus.Delivered);
var failed = allDeliveries.Count(d => d.Status == NotifyDeliveryStatus.Failed);
var throttled = allDeliveries.Count(d => d.Status == NotifyDeliveryStatus.Throttled);
var pending = allDeliveries.Count(d => d.Status == NotifyDeliveryStatus.Pending);
var total = sent + failed;
// Calculate average delivery time from attempts that have status codes (indicating completion)
var completedAttempts = allDeliveries
.Where(d => d.Attempts.Length > 0)
.SelectMany(d => d.Attempts)
.Where(a => a.StatusCode.HasValue)
.ToList();
var avgDeliveryTime = completedAttempts.Count > 0 ? 0.0 : 0.0; // Response time not tracked in this model
var byChannel = allDeliveries
.GroupBy(d => d.ActionId)
.ToDictionary(
g => g.Key,
g => new
{
sent = g.Count(d => d.Status == NotifyDeliveryStatus.Sent || d.Status == NotifyDeliveryStatus.Delivered),
failed = g.Count(d => d.Status == NotifyDeliveryStatus.Failed)
});
var byEventKind = allDeliveries
.GroupBy(d => d.Kind)
.ToDictionary(
g => g.Key,
g => new
{
sent = g.Count(d => d.Status == NotifyDeliveryStatus.Sent || d.Status == NotifyDeliveryStatus.Delivered),
failed = g.Count(d => d.Status == NotifyDeliveryStatus.Failed)
});
return Results.Ok(new
{
totalSent = sent,
totalFailed = failed,
totalThrottled = throttled,
totalPending = pending,
avgDeliveryTimeMs = avgDeliveryTime,
successRate = total > 0 ? (double)sent / total * 100 : 0,
period = "day",
byChannel,
byEventKind
});
});
// =============================================
// Simulation API (NOTIFY-SVC-39-003)
// =============================================