diff --git a/server/emails/templates/AlertNotification.tsx b/server/emails/templates/AlertNotification.tsx index b540142d2..5542384a9 100644 --- a/server/emails/templates/AlertNotification.tsx +++ b/server/emails/templates/AlertNotification.tsx @@ -36,8 +36,8 @@ function getEventMeta(eventType: AlertEventType): { heading: string; previewText: string; summary: string; - statusLabel: string; - statusColor: string; + statusLabel: string | null; + statusColor: string | null; } { switch (eventType) { case "site_online": @@ -63,8 +63,8 @@ function getEventMeta(eventType: AlertEventType): { heading: "Site Status Changed", previewText: "A site in your organization has changed status.", summary: "A site in your organization has changed status.", - statusLabel: "Status Changed", - statusColor: "#f59e0b" + statusLabel: null, + statusColor: null }; case "health_check_healthy": return { @@ -93,8 +93,8 @@ function getEventMeta(eventType: AlertEventType): { "A health check in your organization has changed status.", summary: "A health check in your organization has changed status.", - statusLabel: "Status Changed", - statusColor: "#f59e0b" + statusLabel: null, + statusColor: null }; case "resource_healthy": return { @@ -120,8 +120,8 @@ function getEventMeta(eventType: AlertEventType): { previewText: "A resource in your organization has changed status.", summary: "A resource in your organization has changed status.", - statusLabel: "Status Changed", - statusColor: "#f59e0b" + statusLabel: null, + statusColor: null }; default: return { @@ -135,11 +135,26 @@ function getEventMeta(eventType: AlertEventType): { } } +function resolveToggleStatus(status: unknown): { label: string; color: string } { + switch (String(status).toLowerCase()) { + case "online": + return { label: "Online", color: "#16a34a" }; + case "offline": + return { label: "Offline", color: "#dc2626" }; + case "healthy": + return { label: "Healthy", color: "#16a34a" }; + case "unhealthy": + return { label: "Unhealthy", color: "#dc2626" }; + default: + return { label: String(status ?? "Unknown"), color: "#f59e0b" }; + } +} + function formatDataItems( data: Record ): { label: string; value: React.ReactNode }[] { return Object.entries(data) - .filter(([key]) => key !== "orgId") + .filter(([key]) => key !== "orgId" && key !== "status") .map(([key, value]) => ({ label: key .replace(/([A-Z])/g, " $1") @@ -154,16 +169,36 @@ export const AlertNotification = (props: AlertNotificationProps) => { const meta = getEventMeta(eventType); const dataItems = formatDataItems(data); + const isToggle = + eventType === "site_toggle" || + eventType === "health_check_toggle" || + eventType === "resource_toggle"; + + const resolvedStatus = isToggle + ? resolveToggleStatus(data.status) + : meta.statusLabel != null + ? { label: meta.statusLabel, color: meta.statusColor! } + : null; + const allItems: { label: string; value: React.ReactNode }[] = [ { label: "Organization", value: orgId }, - { - label: "Status", - value: ( - - {meta.statusLabel} - - ) - }, + ...(resolvedStatus != null + ? [ + { + label: "Status", + value: ( + + {resolvedStatus.label} + + ) + } + ] + : []), { label: "Time", value: new Date().toUTCString() }, ...dataItems ]; diff --git a/server/private/lib/alerts/events/healthCheckEvents.ts b/server/private/lib/alerts/events/healthCheckEvents.ts index c2ba25b28..4851f08c4 100644 --- a/server/private/lib/alerts/events/healthCheckEvents.ts +++ b/server/private/lib/alerts/events/healthCheckEvents.ts @@ -76,6 +76,7 @@ export async function fireHealthCheckHealthyAlert( healthCheckId, data: { healthCheckId, + status: "healthy", ...(healthCheckName != null ? { healthCheckName } : {}), ...extra } @@ -133,6 +134,7 @@ export async function fireHealthCheckUnhealthyAlert( healthCheckId, data: { healthCheckId, + status: "unhealthy", ...(healthCheckName != null ? { healthCheckName } : {}), ...extra } diff --git a/server/private/lib/alerts/events/resourceEvents.ts b/server/private/lib/alerts/events/resourceEvents.ts index c2d6d3725..8c20bc5a1 100644 --- a/server/private/lib/alerts/events/resourceEvents.ts +++ b/server/private/lib/alerts/events/resourceEvents.ts @@ -61,6 +61,7 @@ export async function fireResourceHealthyAlert( resourceId, data: { resourceId, + status: "healthy", ...(resourceName != null ? { resourceName } : {}), ...extra } @@ -115,6 +116,7 @@ export async function fireResourceUnhealthyAlert( resourceId, data: { resourceId, + status: "unhealthy", ...(resourceName != null ? { resourceName } : {}), ...extra } diff --git a/server/private/lib/alerts/events/siteEvents.ts b/server/private/lib/alerts/events/siteEvents.ts index 580e00848..562accc18 100644 --- a/server/private/lib/alerts/events/siteEvents.ts +++ b/server/private/lib/alerts/events/siteEvents.ts @@ -63,6 +63,7 @@ export async function fireSiteOnlineAlert( siteId, data: { siteId, + status: "online", ...(siteName != null ? { siteName } : {}), ...extra } @@ -143,6 +144,7 @@ export async function fireSiteOfflineAlert( siteId, data: { siteId, + status: "offline", ...(siteName != null ? { siteName } : {}), ...extra } diff --git a/server/private/lib/alerts/sendAlertWebhook.ts b/server/private/lib/alerts/sendAlertWebhook.ts index 5656026bc..2dd0eb600 100644 --- a/server/private/lib/alerts/sendAlertWebhook.ts +++ b/server/private/lib/alerts/sendAlertWebhook.ts @@ -42,6 +42,7 @@ export async function sendAlertWebhook( const payload = { event: context.eventType, timestamp: new Date().toISOString(), + status: deriveStatus(context.eventType, context.data), data: { orgId: context.orgId, ...context.data @@ -117,6 +118,38 @@ export async function sendAlertWebhook( throw lastError ?? new Error(`Alert webhook: all ${MAX_RETRIES} attempts failed for "${url}"`); } +// --------------------------------------------------------------------------- +// Status derivation +// --------------------------------------------------------------------------- + +function deriveStatus( + eventType: AlertContext["eventType"], + data: Record +): string { + switch (eventType) { + case "site_online": + return "online"; + case "site_offline": + return "offline"; + case "site_toggle": + return String(data.status ?? "unknown"); + case "health_check_healthy": + case "resource_healthy": + return "healthy"; + case "health_check_unhealthy": + case "resource_unhealthy": + return "unhealthy"; + case "health_check_toggle": + case "resource_toggle": + return String(data.status ?? "unknown"); + default: { + const _exhaustive: never = eventType; + void _exhaustive; + return "unknown"; + } + } +} + // --------------------------------------------------------------------------- // Header construction (mirrors HttpLogDestination.buildHeaders) // ---------------------------------------------------------------------------