Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix alert storm mail template #1427

Merged
merged 21 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
update package
  • Loading branch information
Doniaab committed Jan 30, 2025
commit 85a33b2018fb035d2b4f7e6a17b6a16721e559ee
2 changes: 1 addition & 1 deletion alerting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"nodemailer": "^6.9.9",
"pg": "^8.5.1",
"prism-common": "file:../common",
"puppeteer": "^24.0.0",
"puppeteer": "^24.1.1",
"typeorm": "^0.3.0",
"typeorm-naming-strategies": "^1.1.0",
"xml-js": "^1.6.11"
Expand Down
61 changes: 38 additions & 23 deletions alerting/src/templates/storm-alert.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -141,35 +141,50 @@
<div class="header"><div class="title">PRISM</div></div>

<div class="content">
<div class="content-title">Activation Triggers activated (<%= windspeed %>) for <%= cycloneName %>
<div class="content-title">
<%= alertTitle %>
</div>
<p>ANTICIPATORY ACTION ALERT FOR TROPICAL STORM <%= cycloneName %> - <%= cycloneTime %> -
FORECAST BY REGIONAL SPECIALIZED METEOROLOGICAL CENTER LA REUNION</p>
<p><div class="trigger-title-container">
<span class="trigger-title">
<span class="readiness-icon"></span> Readiness Trigger Activated
<span class="readiness-icon"></span> Readiness Trigger:
<% if (readiness) { %>
Activated
<% } else { %>
Not Activated
<% } %>
</span>
</div><br>
<%= cycloneTime %> there was 20% likelihood of Mozambique experiencing tropical storm-force winds within the next 5 days</p>

<p><div class="trigger-title-container">
<span class="trigger-title">
<span class="activation-icon"></span> Activation Triggers: Activated
</span>
</div><br>
Projected wind speeds affecting districts in the next 72 hours:</p>

<table class="table-container">
<tr>
<td>> 89 km/h</td>
<td><%= districts48kt %></td>
</tr>
<tr>
<td>> 118 km/h</td>
<td><%= districts64kt %></td>
</tr>
</table>

</div></p>
<% if (readiness) { %>
<p><%= cycloneTime %> there was 20% likelihood of Mozambique experiencing tropical storm-force winds within the next 5 days</p>
<% } %>
<p>
<div class="trigger-title-container">
<span class="trigger-title">
<span class="activation-icon"></span> Activation Triggers:
<% if (activatedTriggers) { %>
Activated
<% } else { %>
Not Activated
<% } %>
</span>
</div>
</p>

<% if (activatedTriggers) { %>
<p>Projected wind speeds affecting districts in the next 72 hours:</p>
<table class="table-container">
<tr>
<td>> 89 km/h</td>
<td><%= activatedTriggers.districts48kt %></td>
</tr>
<tr>
<td>> 118 km/h</td>
<td><%= activatedTriggers.districts64kt %></td>
</tr>
</table>
<% } %>
<div class="button-container">
<a href="<%= redirectUrl %>" target="_blank" rel="noopener noreferrer" class="button">
<span style="vertical-align: middle;">ACCESS PRISM TROPICAL STORM DASHBOARD</span>
Expand Down
19 changes: 14 additions & 5 deletions alerting/src/types/email.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
export interface StormAlertData {
email: string,
cycloneName: string,
cycloneTime: Date,
districts48kt: string[],
districts64kt: string[],
cycloneTime: string,
activatedTriggers?: {
districts48kt: string[],
districts64kt: string[],
windspeed: string,
},
redirectUrl: string,
windspeed: string,
base64Image: string,
readiness: boolean,
}

export interface StormAlertEmail extends Omit<StormAlertData, 'email'> {
export interface StormAlertEmail extends Omit<StormAlertData, 'email' | 'activatedTriggers'> {
alertTitle: string;
base64Image: string;
icons: {
mapIcon: string;
arrowForwardIcon: string;
};
unsubscribeUrl: string;
activatedTriggers?: {
districts48kt: string,
districts64kt: string,
windspeed: string,
},
}
7 changes: 5 additions & 2 deletions alerting/src/utils/capture-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import puppeteer, { Browser, Page, BoundingBox } from 'puppeteer';

import fs from 'fs';
interface CropRegion {
x: number;
y: number;
Expand Down Expand Up @@ -166,7 +166,10 @@ async function captureScreenshotFromUrl(options: ScreenshotOptions): Promise<str
};

base64Image = await page.screenshot({
encoding: 'base64', // use path to save to file
type: 'jpeg',
quality: 70,
fullPage: false,
encoding: 'base64',
clip: finalCrop,
});

Expand Down
30 changes: 30 additions & 0 deletions alerting/src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Converts an ISO date string to a formatted string with weekday, date, and time in UTC.
*
* @param {string} isoDate - The ISO date string (e.g., "2024-12-12T00:00:00Z").
* @returns {string} - The formatted date string (e.g., "Tuesday 12/03/2024 14:00 UTC").
*/
export function formatDateToUTC(isoDate: string): string {
const dateObj = new Date(isoDate);

if (isNaN(dateObj.getTime())) {
throw new Error("Invalid date format");
}

const options: Intl.DateTimeFormatOptions = {
weekday: "long",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
hour12: false
};

const formattedDate = new Intl.DateTimeFormat("en-US", options).format(dateObj);

const [weekday, month, day, year, time] = formattedDate.split(/[\s,]+/);
return `${weekday} ${day}/${month}/${year} ${time} UTC`;
}

102 changes: 63 additions & 39 deletions alerting/src/utils/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StormAlertData, StormAlertEmail } from '../types/email';
import ejs from 'ejs';
import path from 'path';
import { encodeImageToBase64 } from './image';
import { formatDateToUTC } from './date';

/**
*
Expand Down Expand Up @@ -89,55 +90,78 @@ export async function sendEmail({
* @param {StormAlertData} data - The storm alert details.
* @param {string} data.email - Recipient's email address.
* @param {string} data.cycloneName - Name of the cyclone.
* @param {Date} data.cycloneTime - The reference date of the cyclone.
* @param {string[]} data.districts48kt - Districts affected by 48kt winds.
* @param {string[]} data.districts64kt - Districts affected by 64kt winds.
* @param {string} data.cycloneTime - The reference date of the cyclone in ISO format.
* @param {ActivatedTriggers | undefined} [data.activatedTriggers] - Object containing details of activated triggers.
* @param {string[]} [data.activatedTriggers.districts48kt] - List of districts affected by winds over 48kt.
* @param {string[]} [data.activatedTriggers.districts64kt] - List of districts affected by winds over 64kt.
* @param {string} [data.activatedTriggers.windspeed] - Wind speed at which the trigger activation occurs.
* @param {string} data.redirectUrl - URL to access the anticipatory action storm map.
* @param {string} data.windspeed - Wind speed at alert time.
* @param {string} data.windspeed - Trigger activation Wind speed .
* @param {boolean} data.readiness - Readiness activation.
* @param {string} data.base64Image - Base64-encoded image of the storm.
*
* @returns {Promise<void>} - Resolves when the email is sent.
*/

export const sendStormAlertEmail = async (data: StormAlertData): Promise<void> => {
const emailData: StormAlertEmail = {
cycloneName: data.cycloneName,
cycloneTime: data.cycloneTime,
districts48kt: data.districts48kt,
districts64kt: data.districts64kt,
redirectUrl: data.redirectUrl,
base64Image: data.base64Image,
icons: {
mapIcon: `data:image/png;base64,${encodeImageToBase64('icons/mapIcon.png')}`,
arrowForwardIcon: `data:image/png;base64,${encodeImageToBase64('icons/arrowForwardIcon.png')}`,
},
unsubscribeUrl: '',
windspeed: data.windspeed,
};

const mailOptions = {
from: '[email protected]',
to: data.email,
subject: `Activation Triggers activated ${data.windspeed} for ${data.cycloneName}`,
html: '',
text: '',
};

try {
const html: string = await new Promise((resolve, reject) => {
ejs.renderFile(path.join(__dirname, 'templates', 'storm-alert.ejs'), emailData, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});

let alertTitle = '';
if (data.activatedTriggers) {
alertTitle = `Activation Triggers activated ${data.activatedTriggers.windspeed} for ${data.cycloneName}`;
} else if (data.readiness) {
alertTitle = `Readiness Triggers activated for ${data.cycloneName}`;
} else {
return Promise.reject('No triggers or readiness activated');
}

const emailData: StormAlertEmail = {
alertTitle,
cycloneName: data.cycloneName,
cycloneTime: formatDateToUTC(data.cycloneTime),
activatedTriggers: data.activatedTriggers
? {
...data.activatedTriggers,
districts48kt: data.activatedTriggers.districts48kt?.length
? data.activatedTriggers.districts48kt.join(', ')
: '',
districts64kt: data.activatedTriggers.districts64kt?.length
? data.activatedTriggers.districts64kt.join(', ')
: '',
}
: undefined,
redirectUrl: data.redirectUrl,
base64Image: data.base64Image,
icons: {
mapIcon: `data:image/png;base64,${encodeImageToBase64('icons/mapIcon.png')}`,
arrowForwardIcon: `data:image/png;base64,${encodeImageToBase64('icons/arrowForwardIcon.png')}`,
},
unsubscribeUrl: '',
readiness: data.readiness,
};

const mailOptions = {
from: '[email protected]',
to: data.email,
subject: alertTitle,
html: '',
text: '',
};

try {
const html: string = await new Promise((resolve, reject) => {
ejs.renderFile(path.join(__dirname, '../templates', 'storm-alert.ejs'), emailData, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});
});

mailOptions.html = html;
await sendEmail(mailOptions);
mailOptions.html = html;
await sendEmail(mailOptions);
} catch (error) {
console.error('Error sending storm alert email:', error);
throw error;
console.error('Error sending storm alert email:', error);
throw error;
}
};

Loading