search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -13,6 +13,7 @@ using StellaOps.Notify.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
@@ -54,7 +55,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var rules = await ruleRepository.ListAsync(tenantId, cancellationToken);
|
||||
@@ -62,7 +63,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithDescription("Returns all alert routing rules for the tenant. Rules define which events trigger notifications, which channels receive them, and any throttle or digest settings applied.");
|
||||
.WithDescription(_t("notifier.rule.list_description"));
|
||||
|
||||
group.MapGet("/rules/{ruleId}", async (
|
||||
HttpContext context,
|
||||
@@ -73,18 +74,18 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var rule = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
||||
if (rule is null)
|
||||
{
|
||||
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
||||
return Results.NotFound(Error("rule_not_found", _t("notifier.error.rule_not_found", ruleId), context));
|
||||
}
|
||||
|
||||
return Results.Ok(MapRuleToResponse(rule));
|
||||
})
|
||||
.WithDescription("Retrieves a single alert routing rule by its identifier. Returns match criteria, actions, throttle settings, and audit metadata.");
|
||||
.WithDescription(_t("notifier.rule.get_description"));
|
||||
|
||||
group.MapPost("/rules", async (
|
||||
HttpContext context,
|
||||
@@ -97,7 +98,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var actor = GetActor(context);
|
||||
@@ -115,7 +116,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.Created($"/api/v2/notify/rules/{rule.RuleId}", MapRuleToResponse(rule));
|
||||
})
|
||||
.WithDescription("Creates a new alert routing rule. The rule specifies event match criteria (kinds, namespaces, severities) and the notification actions to execute. An audit entry is written on creation.")
|
||||
.WithDescription(_t("notifier.rule.create_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapPut("/rules/{ruleId}", async (
|
||||
@@ -130,13 +131,13 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var existing = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
||||
return Results.NotFound(Error("rule_not_found", _t("notifier.error.rule_not_found", ruleId), context));
|
||||
}
|
||||
|
||||
var actor = GetActor(context);
|
||||
@@ -154,7 +155,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.Ok(MapRuleToResponse(updated));
|
||||
})
|
||||
.WithDescription("Updates an existing alert routing rule. Only the provided fields are changed; match criteria, actions, throttle settings, and labels are merged. An audit entry is written on update.")
|
||||
.WithDescription(_t("notifier.rule.update_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapDelete("/rules/{ruleId}", async (
|
||||
@@ -167,13 +168,13 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var existing = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
||||
return Results.NotFound(Error("rule_not_found", _t("notifier.error.rule_not_found", ruleId), context));
|
||||
}
|
||||
|
||||
var actor = GetActor(context);
|
||||
@@ -187,7 +188,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithDescription("Permanently removes an alert routing rule. Future events will no longer be matched against this rule. An audit entry is written on deletion.")
|
||||
.WithDescription(_t("notifier.rule.delete_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
}
|
||||
|
||||
@@ -205,7 +206,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
NotifyChannelType? channelTypeEnum = null;
|
||||
@@ -227,7 +228,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithDescription("Lists all notification templates for the tenant, with optional filtering by key prefix, channel type, and locale. Templates define the rendered message body used by notification rules.");
|
||||
.WithDescription(_t("notifier.template.list_description"));
|
||||
|
||||
group.MapGet("/templates/{templateId}", async (
|
||||
HttpContext context,
|
||||
@@ -238,18 +239,18 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var template = await templateService.GetByIdAsync(tenantId, templateId, cancellationToken);
|
||||
if (template is null)
|
||||
{
|
||||
return Results.NotFound(Error("template_not_found", $"Template {templateId} not found.", context));
|
||||
return Results.NotFound(Error("template_not_found", _t("notifier.error.template_not_found", templateId), context));
|
||||
}
|
||||
|
||||
return Results.Ok(MapTemplateToResponse(template));
|
||||
})
|
||||
.WithDescription("Retrieves a single notification template by its identifier. Returns the template body, channel type, locale, render mode, and audit metadata.");
|
||||
.WithDescription(_t("notifier.template.get_description"));
|
||||
|
||||
group.MapPost("/templates", async (
|
||||
HttpContext context,
|
||||
@@ -260,14 +261,14 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var actor = GetActor(context);
|
||||
|
||||
if (!Enum.TryParse<NotifyChannelType>(request.ChannelType, true, out var channelType))
|
||||
{
|
||||
return Results.BadRequest(Error("invalid_channel_type", $"Invalid channel type: {request.ChannelType}", context));
|
||||
return Results.BadRequest(Error("invalid_channel_type", _t("notifier.error.invalid_channel_type", request.ChannelType), context));
|
||||
}
|
||||
|
||||
var renderMode = NotifyTemplateRenderMode.Markdown;
|
||||
@@ -300,7 +301,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return Results.BadRequest(Error("template_validation_failed", result.Error ?? "Validation failed.", context));
|
||||
return Results.BadRequest(Error("template_validation_failed", result.Error ?? _t("notifier.error.template_validation_failed"), context));
|
||||
}
|
||||
|
||||
var created = await templateService.GetByIdAsync(tenantId, request.TemplateId, cancellationToken);
|
||||
@@ -309,7 +310,7 @@ public static class NotifyApiEndpoints
|
||||
? Results.Created($"/api/v2/notify/templates/{request.TemplateId}", MapTemplateToResponse(created!))
|
||||
: Results.Ok(MapTemplateToResponse(created!));
|
||||
})
|
||||
.WithDescription("Creates or updates a notification template. The template body supports Scriban syntax with access to event payload fields. Validation is performed before persisting; an error is returned for invalid syntax.")
|
||||
.WithDescription(_t("notifier.template.upsert_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapDelete("/templates/{templateId}", async (
|
||||
@@ -321,7 +322,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var actor = GetActor(context);
|
||||
@@ -329,12 +330,12 @@ public static class NotifyApiEndpoints
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return Results.NotFound(Error("template_not_found", $"Template {templateId} not found.", context));
|
||||
return Results.NotFound(Error("template_not_found", _t("notifier.error.template_not_found", templateId), context));
|
||||
}
|
||||
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithDescription("Permanently removes a notification template. Rules referencing this template will fall back to channel defaults on the next delivery. An audit entry is written on deletion.")
|
||||
.WithDescription(_t("notifier.template.delete_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapPost("/templates/preview", async (
|
||||
@@ -347,7 +348,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
NotifyTemplate? template = null;
|
||||
@@ -358,7 +359,7 @@ public static class NotifyApiEndpoints
|
||||
template = await templateService.GetByIdAsync(tenantId, request.TemplateId, cancellationToken);
|
||||
if (template is null)
|
||||
{
|
||||
return Results.NotFound(Error("template_not_found", $"Template {request.TemplateId} not found.", context));
|
||||
return Results.NotFound(Error("template_not_found", _t("notifier.error.template_not_found", request.TemplateId), context));
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(request.TemplateBody))
|
||||
@@ -388,7 +389,7 @@ public static class NotifyApiEndpoints
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.BadRequest(Error("template_required", "Either templateId or templateBody must be provided.", context));
|
||||
return Results.BadRequest(Error("template_required", _t("notifier.error.template_required"), context));
|
||||
}
|
||||
|
||||
var sampleEvent = NotifyEvent.Create(
|
||||
@@ -412,7 +413,7 @@ public static class NotifyApiEndpoints
|
||||
Warnings = warnings
|
||||
});
|
||||
})
|
||||
.WithDescription("Renders a template against a sample event payload without sending any notification. Accepts either an existing templateId or an inline templateBody. Returns the rendered body, subject, and any template warnings.")
|
||||
.WithDescription(_t("notifier.template.preview_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapPost("/templates/validate", (
|
||||
@@ -422,7 +423,7 @@ public static class NotifyApiEndpoints
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.TemplateBody))
|
||||
{
|
||||
return Results.BadRequest(Error("template_body_required", "templateBody is required.", context));
|
||||
return Results.BadRequest(Error("template_body_required", _t("notifier.error.template_body_required"), context));
|
||||
}
|
||||
|
||||
var result = templateService.Validate(request.TemplateBody);
|
||||
@@ -434,7 +435,7 @@ public static class NotifyApiEndpoints
|
||||
warnings = result.Warnings
|
||||
});
|
||||
})
|
||||
.WithDescription("Validates a template body for syntax correctness without persisting it. Returns isValid, a list of errors, and any non-fatal warnings.");
|
||||
.WithDescription(_t("notifier.template.validate_description"));
|
||||
}
|
||||
|
||||
private static void MapIncidentsEndpoints(RouteGroupBuilder group)
|
||||
@@ -453,7 +454,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
// For now, return recent deliveries grouped by event kind as "incidents"
|
||||
@@ -487,7 +488,7 @@ public static class NotifyApiEndpoints
|
||||
NextCursor = queryResult.ContinuationToken
|
||||
});
|
||||
})
|
||||
.WithDescription("Returns a paginated list of notification incidents for the tenant, grouped by event ID. Supports filtering by status, event kind prefix, time range, and cursor-based pagination.");
|
||||
.WithDescription(_t("notifier.incident.list_description"));
|
||||
|
||||
group.MapPost("/incidents/{incidentId}/ack", async (
|
||||
HttpContext context,
|
||||
@@ -499,7 +500,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var actor = request.Actor ?? GetActor(context);
|
||||
@@ -512,7 +513,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithDescription("Acknowledges an incident, recording the actor and an optional comment in the audit log. Does not stop an active escalation; use the escalation stop endpoint for that.")
|
||||
.WithDescription(_t("notifier.incident.ack_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
|
||||
group.MapPost("/incidents/{incidentId}/resolve", async (
|
||||
@@ -525,7 +526,7 @@ public static class NotifyApiEndpoints
|
||||
var tenantId = GetTenantId(context);
|
||||
if (tenantId is null)
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
return Results.BadRequest(Error("tenant_missing", _t("notifier.error.tenant_missing"), context));
|
||||
}
|
||||
|
||||
var actor = request.Actor ?? GetActor(context);
|
||||
@@ -539,7 +540,7 @@ public static class NotifyApiEndpoints
|
||||
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithDescription("Marks an incident as resolved, recording the actor, resolution reason, and optional comment in the audit log. Subsequent notifications for this event kind will continue to be processed normally.")
|
||||
.WithDescription(_t("notifier.incident.resolve_description"))
|
||||
.RequireAuthorization(NotifierPolicies.NotifyOperator);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user