diff --git a/server/emails/templates/AlertNotification.tsx b/server/emails/templates/AlertNotification.tsx index 418924650..5ee7d6375 100644 --- a/server/emails/templates/AlertNotification.tsx +++ b/server/emails/templates/AlertNotification.tsx @@ -8,9 +8,11 @@ import { EmailHeading, EmailInfoSection, EmailLetterHead, + EmailSection, EmailSignature, EmailText } from "./components/Email"; +import ButtonLink from "./components/ButtonLink"; export type AlertEventType = | "site_online" @@ -23,11 +25,38 @@ export type AlertEventType = | "resource_unhealthy" | "resource_toggle"; -interface Props { +// --------------------------------------------------------------------------- +// Local preview / layout testing +// --------------------------------------------------------------------------- +// +// Set to `true` while running `npm run email` or otherwise rendering this +// template without real alert context. Uses `alertNotificationFixture` below +// and ignores props passed by callers (including real alert sends). Must be +// `false` before shipping or triggering real alert emails. + +export const USE_FAKE_ALERT_NOTIFICATION_DATA = true; + +export type AlertNotificationProps = { eventType: AlertEventType; orgId: string; data: Record; -} + dashboardLink: string; +}; + +/** Sample props for previews; also used when `USE_FAKE_ALERT_NOTIFICATION_DATA` is true. */ +export const alertNotificationFixture: AlertNotificationProps = { + eventType: "site_online", + orgId: "org_preview_7a3c2f91", + dashboardLink: + "https://app.pangolin.net/org_preview_7a3c2f91/settings/alerting/rules", + data: { + siteId: 42, + healthCheckName: "Edge API – readiness probe", + targetUrl: "https://api.example.com/internal/health", + lastFailureMessage: "Connection timed out after 5000ms", + consecutiveFailures: 3 + } +}; function getEventMeta(eventType: AlertEventType): { heading: string; @@ -51,7 +80,7 @@ function getEventMeta(eventType: AlertEventType): { heading: "Site Offline", previewText: "A site in your organization has gone offline.", summary: - "A site in your organization has gone offline and is no longer reachable. Please investigate as soon as possible.", + "A site in your organization has gone offline and is no longer reachable.", statusLabel: "Offline", statusColor: "#dc2626" }; @@ -59,8 +88,7 @@ function getEventMeta(eventType: AlertEventType): { return { heading: "Site Status Changed", previewText: "A site in your organization has changed status.", - summary: - "A site in your organization has changed status. Please review the details below and take action if needed.", + summary: "A site in your organization has changed status.", statusLabel: "Status Changed", statusColor: "#f59e0b" }; @@ -80,7 +108,7 @@ function getEventMeta(eventType: AlertEventType): { previewText: "A health check in your organization is not healthy.", summary: - "A health check in your organization is currently failing. Please review the details below and take action if needed.", + "A health check in your organization is currently failing.", statusLabel: "Not Healthy", statusColor: "#dc2626" }; @@ -90,7 +118,7 @@ function getEventMeta(eventType: AlertEventType): { previewText: "A health check in your organization has changed status.", summary: - "A health check in your organization has changed status. Please review the details below and take action if needed.", + "A health check in your organization has changed status.", statusLabel: "Status Changed", statusColor: "#f59e0b" }; @@ -108,7 +136,7 @@ function getEventMeta(eventType: AlertEventType): { heading: "Resource Unhealthy", previewText: "A resource in your organization is not healthy.", summary: - "A resource in your organization is currently unhealthy. Please review the details below and take action if needed.", + "A resource in your organization is currently unhealthy.", statusLabel: "Unhealthy", statusColor: "#dc2626" }; @@ -117,17 +145,16 @@ function getEventMeta(eventType: AlertEventType): { heading: "Resource Status Changed", previewText: "A resource in your organization has changed status.", - summary: - "A resource in your organization has changed status. Please review the details below and take action if needed.", + summary: "A resource in your organization has changed status.", statusLabel: "Status Changed", statusColor: "#f59e0b" }; default: return { heading: "Alert Notification", - previewText: "An alert event has occurred in your organization.", - summary: - "An alert event has occurred in your organization. Please review the details below and take action if needed.", + previewText: + "An alert event has occurred in your organization.", + summary: "An alert event has occurred in your organization.", statusLabel: "Alert", statusColor: "#f59e0b" }; @@ -148,17 +175,22 @@ function formatDataItems( })); } -export const AlertNotification = ({ eventType, orgId, data }: Props) => { +export const AlertNotification = (props: AlertNotificationProps) => { + const { eventType, orgId, data, dashboardLink } = + USE_FAKE_ALERT_NOTIFICATION_DATA ? alertNotificationFixture : props; const meta = getEventMeta(eventType); const dataItems = formatDataItems(data); const allItems: { label: string; value: React.ReactNode }[] = [ { label: "Organization", value: orgId }, - { label: "Status", value: ( - - {meta.statusLabel} - - )}, + { + label: "Status", + value: ( + + {meta.statusLabel} + + ) + }, { label: "Time", value: new Date().toUTCString() }, ...dataItems ]; @@ -184,10 +216,16 @@ export const AlertNotification = ({ eventType, orgId, data }: Props) => { /> - Log in to your dashboard to view more details and - manage your alert rules. + Open your dashboard to view more details and manage + your alert rules. + + + Open Dashboard + + + diff --git a/server/emails/templates/NotifyTrialExpiring.tsx b/server/emails/templates/NotifyTrialExpiring.tsx index 5e8f0e6a8..7cd6d30ac 100644 --- a/server/emails/templates/NotifyTrialExpiring.tsx +++ b/server/emails/templates/NotifyTrialExpiring.tsx @@ -36,8 +36,8 @@ export const NotifyTrialExpiring = ({ : `Your trial for ${orgName} ends in ${daysRemaining} days.`; const heading = hasEnded - ? "Your Trial Has Ended" - : "Your Trial Is Ending Soon"; + ? "Your Trial Ended" + : "Your Trial is Ending Soon"; return ( @@ -124,4 +124,4 @@ export const NotifyTrialExpiring = ({ ); }; -export default NotifyTrialExpiring; \ No newline at end of file +export default NotifyTrialExpiring; diff --git a/server/emails/templates/components/Email.tsx b/server/emails/templates/components/Email.tsx index 71d8b4671..f74046042 100644 --- a/server/emails/templates/components/Email.tsx +++ b/server/emails/templates/components/Email.tsx @@ -5,7 +5,7 @@ import { build } from "@server/build"; // EmailContainer: Wraps the entire email layout export function EmailContainer({ children }: { children: React.ReactNode }) { return ( - + {children} ); @@ -18,7 +18,7 @@ export function EmailLetterHead() { Pangolin Logo diff --git a/server/private/lib/alerts/events/healthCheckEvents.ts b/server/private/lib/alerts/events/healthCheckEvents.ts index 02b46e94e..2e1f591a2 100644 --- a/server/private/lib/alerts/events/healthCheckEvents.ts +++ b/server/private/lib/alerts/events/healthCheckEvents.ts @@ -41,7 +41,6 @@ export async function fireHealthCheckHealthyAlert( orgId, healthCheckId, data: { - healthCheckId, ...(healthCheckName != null ? { healthCheckName } : {}), ...extra } @@ -87,7 +86,6 @@ export async function fireHealthCheckNotHealthyAlert( orgId, healthCheckId, data: { - healthCheckId, ...(healthCheckName != null ? { healthCheckName } : {}), ...extra } diff --git a/server/private/lib/alerts/events/resourceEvents.ts b/server/private/lib/alerts/events/resourceEvents.ts index eb84982aa..280b1d0c9 100644 --- a/server/private/lib/alerts/events/resourceEvents.ts +++ b/server/private/lib/alerts/events/resourceEvents.ts @@ -41,7 +41,6 @@ export async function fireResourceHealthyAlert( orgId, resourceId, data: { - resourceId, ...(resourceName != null ? { resourceName } : {}), ...extra } @@ -87,7 +86,6 @@ export async function fireResourceUnhealthyAlert( orgId, resourceId, data: { - resourceId, ...(resourceName != null ? { resourceName } : {}), ...extra } @@ -133,7 +131,6 @@ export async function fireResourceToggleAlert( orgId, resourceId, data: { - resourceId, ...(resourceName != null ? { resourceName } : {}), ...extra } diff --git a/server/private/lib/alerts/events/siteEvents.ts b/server/private/lib/alerts/events/siteEvents.ts index aeb8a8d2c..d8531a5b7 100644 --- a/server/private/lib/alerts/events/siteEvents.ts +++ b/server/private/lib/alerts/events/siteEvents.ts @@ -41,7 +41,6 @@ export async function fireSiteOnlineAlert( orgId, siteId, data: { - siteId, ...(siteName != null ? { siteName } : {}), ...extra } @@ -87,7 +86,6 @@ export async function fireSiteOfflineAlert( orgId, siteId, data: { - siteId, ...(siteName != null ? { siteName } : {}), ...extra } diff --git a/server/private/lib/alerts/sendAlertEmail.ts b/server/private/lib/alerts/sendAlertEmail.ts index 634598158..5e818678d 100644 --- a/server/private/lib/alerts/sendAlertEmail.ts +++ b/server/private/lib/alerts/sendAlertEmail.ts @@ -36,13 +36,17 @@ export async function sendAlertEmail( const from = config.getNoReplyEmail(); const subject = buildSubject(context); + const baseUrl = config.getRawConfig().app.dashboard_url!.replace(/\/$/, ""); + const dashboardLink = `${baseUrl}/${context.orgId}/settings`; + for (const to of recipients) { try { await sendEmail( AlertNotification({ eventType: context.eventType, orgId: context.orgId, - data: context.data + data: context.data, + dashboardLink }), { from,