mongodb-backup-service 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mongodb-backup-service",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Reusable Node.js service for automated MongoDB backups with scheduling, startup recovery, Excel export, retention, email notifications, and backup locking.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// src/notifications/emailNotifier.js
|
|
1
2
|
const nodemailer = require('nodemailer');
|
|
2
3
|
const logger = require('../utils/logger');
|
|
3
4
|
const EmailTemplates = require('./emailTemplates');
|
|
@@ -6,6 +7,7 @@ class EmailNotifier {
|
|
|
6
7
|
constructor(config) {
|
|
7
8
|
this.config = config.email;
|
|
8
9
|
this.transporter = null;
|
|
10
|
+
this.recipients = [];
|
|
9
11
|
|
|
10
12
|
if (!this.config.enabled) {
|
|
11
13
|
logger.info("Email notifications are disabled (email.enabled = false).");
|
|
@@ -19,6 +21,22 @@ class EmailNotifier {
|
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
// Normalize recipient(s) into an array of trimmed, non‑empty email addresses
|
|
25
|
+
const normalizeRecipients = (to) => {
|
|
26
|
+
if (Array.isArray(to)) {
|
|
27
|
+
return to.map(r => String(r).trim()).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
if (typeof to === 'string') {
|
|
30
|
+
return to.split(',').map(r => r.trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
};
|
|
34
|
+
this.recipients = normalizeRecipients(this.config.to);
|
|
35
|
+
if (this.recipients.length === 0) {
|
|
36
|
+
logger.warn('Email notifications enabled but recipient list is empty. Notifications will be skipped.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
this.transporter = nodemailer.createTransport({
|
|
23
41
|
host: this.config.host,
|
|
24
42
|
port: this.config.port,
|
|
@@ -29,7 +47,7 @@ class EmailNotifier {
|
|
|
29
47
|
}
|
|
30
48
|
});
|
|
31
49
|
|
|
32
|
-
logger.info(`Email notifier initialised. SMTP host: ${this.config.host}, port: ${this.config.port}, secure: ${this.config.secure},
|
|
50
|
+
logger.info(`Email notifier initialised. SMTP host: ${this.config.host}, port: ${this.config.port}, secure: ${this.config.secure}, recipients: ${this.recipients.join(', ')}`);
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/**
|
|
@@ -61,16 +79,16 @@ class EmailNotifier {
|
|
|
61
79
|
return;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
const { subject, text } = EmailTemplates.getBackupReport(metadata);
|
|
82
|
+
const { subject, text, html } = EmailTemplates.getBackupReport(metadata);
|
|
65
83
|
|
|
66
84
|
const mailOptions = {
|
|
67
85
|
from: this.config.from || `"Database Backup Service" <${this.config.user}>`,
|
|
68
|
-
to:
|
|
86
|
+
to: this.recipients,
|
|
69
87
|
subject,
|
|
70
|
-
text
|
|
88
|
+
text,
|
|
89
|
+
html
|
|
71
90
|
};
|
|
72
|
-
|
|
73
|
-
logger.info(`Sending email notification to: ${this.config.to} | Subject: ${subject}`);
|
|
91
|
+
logger.info(`Sending email notification to: ${this.recipients.join(', ')} | Subject: ${subject}`);
|
|
74
92
|
|
|
75
93
|
try {
|
|
76
94
|
const info = await this.transporter.sendMail(mailOptions);
|
|
@@ -1,33 +1,596 @@
|
|
|
1
|
+
// src/notifications/emailTemplates.js
|
|
2
|
+
/**
|
|
3
|
+
* EmailTemplates – Generates email subject, plain-text and HTML bodies for backup reports.
|
|
4
|
+
*/
|
|
1
5
|
class EmailTemplates {
|
|
2
|
-
|
|
6
|
+
/** Escape HTML special characters to avoid injection. */
|
|
7
|
+
static _escapeHtml(str) {
|
|
8
|
+
if (typeof str !== 'string') return '';
|
|
9
|
+
return str
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Build the email subject line. */
|
|
18
|
+
static _buildSubject(metadata) {
|
|
19
|
+
const statusWord = metadata.status === 'success' ? 'Successful' : 'Failed';
|
|
20
|
+
return `Database Backup ${statusWord} — ${metadata.projectName} | ${metadata.backupDate}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Format duration from milliseconds into a human-readable string. */
|
|
24
|
+
static _formatDuration(ms) {
|
|
25
|
+
const mins = Math.floor(ms / 60000);
|
|
26
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
27
|
+
|
|
28
|
+
if (mins === 0) {
|
|
29
|
+
return `${secs} seconds`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `${mins} minutes ${secs} seconds`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Render a component status badge. */
|
|
36
|
+
static _renderStatusBadge(status) {
|
|
37
|
+
const normalized = (status || 'skipped').toLowerCase();
|
|
38
|
+
|
|
39
|
+
const map = {
|
|
40
|
+
success: {
|
|
41
|
+
icon: '✓',
|
|
42
|
+
label: 'Completed',
|
|
43
|
+
bg: '#ecfdf3',
|
|
44
|
+
color: '#067647'
|
|
45
|
+
},
|
|
46
|
+
failed: {
|
|
47
|
+
icon: '✕',
|
|
48
|
+
label: 'Failed',
|
|
49
|
+
bg: '#fef3f2',
|
|
50
|
+
color: '#b42318'
|
|
51
|
+
},
|
|
52
|
+
skipped: {
|
|
53
|
+
icon: '—',
|
|
54
|
+
label: 'Skipped',
|
|
55
|
+
bg: '#f2f4f7',
|
|
56
|
+
color: '#667085'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const item = map[normalized] || map.skipped;
|
|
61
|
+
|
|
62
|
+
return `
|
|
63
|
+
<span style="
|
|
64
|
+
display:inline-block;
|
|
65
|
+
background:${item.bg};
|
|
66
|
+
color:${item.color};
|
|
67
|
+
padding:5px 10px;
|
|
68
|
+
border-radius:999px;
|
|
69
|
+
font-size:12px;
|
|
70
|
+
font-weight:700;
|
|
71
|
+
white-space:nowrap;
|
|
72
|
+
">
|
|
73
|
+
${item.icon} ${item.label}
|
|
74
|
+
</span>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Build the full HTML body. */
|
|
79
|
+
static _buildHtml(metadata) {
|
|
3
80
|
const isSuccess = metadata.status === 'success';
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
const esc = EmailTemplates._escapeHtml;
|
|
82
|
+
|
|
83
|
+
const projectName = esc(metadata.projectName || 'Database');
|
|
84
|
+
const backupDate = esc(metadata.backupDate || 'N/A');
|
|
85
|
+
const startTime = esc(metadata.startTime || metadata.startedAt || 'N/A');
|
|
86
|
+
const endTime = esc(metadata.endTime || metadata.completedAt || 'N/A');
|
|
87
|
+
const duration = EmailTemplates._formatDuration(metadata.durationMs || 0);
|
|
88
|
+
|
|
89
|
+
const scheduleType = esc(
|
|
90
|
+
(metadata.schedule && metadata.schedule.type) || 'N/A'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const timezone = esc(
|
|
94
|
+
(metadata.schedule && metadata.schedule.timezone) || 'N/A'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const statusText = isSuccess ? 'SUCCESS' : 'FAILED';
|
|
98
|
+
|
|
99
|
+
const theme = isSuccess
|
|
100
|
+
? {
|
|
101
|
+
accent: '#16a34a',
|
|
102
|
+
accentDark: '#15803d',
|
|
103
|
+
light: '#f0fdf4',
|
|
104
|
+
border: '#bbf7d0',
|
|
105
|
+
icon: '✓',
|
|
106
|
+
title: 'Backup Completed Successfully',
|
|
107
|
+
description: 'Your scheduled database backup completed successfully.'
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
accent: '#dc2626',
|
|
111
|
+
accentDark: '#b91c1c',
|
|
112
|
+
light: '#fef2f2',
|
|
113
|
+
border: '#fecaca',
|
|
114
|
+
icon: '✕',
|
|
115
|
+
title: 'Backup Failed',
|
|
116
|
+
description: 'The database backup could not be completed successfully.'
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const components = [
|
|
120
|
+
{ name: 'MongoDB Dump', data: metadata.mongoDump },
|
|
121
|
+
{ name: 'Excel Export', data: metadata.excel },
|
|
122
|
+
{ name: 'Metadata Generation', data: metadata.metadata },
|
|
123
|
+
{ name: 'Retention Cleanup', data: metadata.retention }
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const files = [
|
|
127
|
+
metadata.mongoDump && metadata.mongoDump.file,
|
|
128
|
+
metadata.excel && metadata.excel.file,
|
|
129
|
+
metadata.metadata && metadata.metadata.file
|
|
130
|
+
].filter(Boolean);
|
|
131
|
+
|
|
132
|
+
const componentRows = components.map(component => {
|
|
133
|
+
const status =
|
|
134
|
+
component.data && component.data.status
|
|
135
|
+
? component.data.status.toLowerCase()
|
|
136
|
+
: 'skipped';
|
|
137
|
+
|
|
138
|
+
return `
|
|
139
|
+
<tr>
|
|
140
|
+
<td style="
|
|
141
|
+
padding:13px 0;
|
|
142
|
+
border-bottom:1px solid #eaecf0;
|
|
143
|
+
font-size:14px;
|
|
144
|
+
color:#344054;
|
|
145
|
+
">
|
|
146
|
+
${esc(component.name)}
|
|
147
|
+
</td>
|
|
148
|
+
|
|
149
|
+
<td style="
|
|
150
|
+
padding:13px 0;
|
|
151
|
+
border-bottom:1px solid #eaecf0;
|
|
152
|
+
text-align:right;
|
|
153
|
+
">
|
|
154
|
+
${EmailTemplates._renderStatusBadge(status)}
|
|
155
|
+
</td>
|
|
156
|
+
</tr>
|
|
157
|
+
`;
|
|
158
|
+
}).join('');
|
|
159
|
+
|
|
160
|
+
const filesHtml = files.length
|
|
161
|
+
? files.map(file => `
|
|
162
|
+
<div style="
|
|
163
|
+
padding:10px 12px;
|
|
164
|
+
margin-bottom:8px;
|
|
165
|
+
background:#f8fafc;
|
|
166
|
+
border:1px solid #eaecf0;
|
|
167
|
+
border-radius:8px;
|
|
168
|
+
font-size:13px;
|
|
169
|
+
color:#475467;
|
|
170
|
+
word-break:break-word;
|
|
171
|
+
">
|
|
172
|
+
${esc(file)}
|
|
173
|
+
</div>
|
|
174
|
+
`).join('')
|
|
175
|
+
: `
|
|
176
|
+
<div style="color:#667085;font-size:13px;">
|
|
177
|
+
No backup files were generated.
|
|
178
|
+
</div>
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
let errorSection = '';
|
|
182
|
+
|
|
183
|
+
if (!isSuccess) {
|
|
184
|
+
const errors = components
|
|
185
|
+
.filter(component => component.data && component.data.error)
|
|
186
|
+
.map(component => `
|
|
187
|
+
<div style="
|
|
188
|
+
margin-bottom:10px;
|
|
189
|
+
padding:12px;
|
|
190
|
+
background:#fffafa;
|
|
191
|
+
border:1px solid #fecdca;
|
|
192
|
+
border-radius:8px;
|
|
193
|
+
font-size:13px;
|
|
194
|
+
line-height:1.5;
|
|
195
|
+
color:#475467;
|
|
196
|
+
">
|
|
197
|
+
<strong style="color:#b42318;">
|
|
198
|
+
${esc(component.name)}
|
|
199
|
+
</strong>
|
|
200
|
+
<br>
|
|
201
|
+
${esc(component.data.error)}
|
|
202
|
+
</div>
|
|
203
|
+
`)
|
|
204
|
+
.join('');
|
|
205
|
+
|
|
206
|
+
errorSection = `
|
|
207
|
+
<tr>
|
|
208
|
+
<td style="padding-top:20px;">
|
|
209
|
+
|
|
210
|
+
<div style="
|
|
211
|
+
font-size:15px;
|
|
212
|
+
font-weight:700;
|
|
213
|
+
color:#101828;
|
|
214
|
+
margin-bottom:12px;
|
|
215
|
+
">
|
|
216
|
+
Error Details
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
${errors || `
|
|
220
|
+
<div style="
|
|
221
|
+
padding:12px;
|
|
222
|
+
background:#f8fafc;
|
|
223
|
+
border-radius:8px;
|
|
224
|
+
font-size:13px;
|
|
225
|
+
color:#667085;
|
|
226
|
+
">
|
|
227
|
+
No detailed error information was provided.
|
|
228
|
+
</div>
|
|
229
|
+
`}
|
|
230
|
+
|
|
231
|
+
<div style="
|
|
232
|
+
margin-top:18px;
|
|
233
|
+
padding:14px;
|
|
234
|
+
background:#fffbeb;
|
|
235
|
+
border:1px solid #fedf89;
|
|
236
|
+
border-radius:8px;
|
|
237
|
+
font-size:13px;
|
|
238
|
+
line-height:1.5;
|
|
239
|
+
color:#475467;
|
|
240
|
+
">
|
|
241
|
+
<strong style="color:#92400e;">
|
|
242
|
+
Action Required
|
|
243
|
+
</strong>
|
|
244
|
+
<br>
|
|
245
|
+
Please review the application logs and investigate
|
|
246
|
+
the failed backup component before the next scheduled run.
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return `<!DOCTYPE html>
|
|
255
|
+
<html lang="en">
|
|
256
|
+
<head>
|
|
257
|
+
<meta charset="UTF-8">
|
|
258
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
259
|
+
<title>Database Backup Report</title>
|
|
260
|
+
</head>
|
|
261
|
+
|
|
262
|
+
<body style="
|
|
263
|
+
margin:0;
|
|
264
|
+
padding:0;
|
|
265
|
+
background:#f2f4f7;
|
|
266
|
+
font-family:Arial,Helvetica,sans-serif;
|
|
267
|
+
color:#101828;
|
|
268
|
+
">
|
|
269
|
+
|
|
270
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
271
|
+
<tr>
|
|
272
|
+
<td align="center" style="padding:32px 16px;">
|
|
273
|
+
|
|
274
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0"
|
|
275
|
+
style="
|
|
276
|
+
max-width:620px;
|
|
277
|
+
background:#ffffff;
|
|
278
|
+
border:1px solid #eaecf0;
|
|
279
|
+
border-radius:14px;
|
|
280
|
+
overflow:hidden;
|
|
281
|
+
">
|
|
282
|
+
|
|
283
|
+
<!-- HEADER -->
|
|
284
|
+
<tr>
|
|
285
|
+
<td style="
|
|
286
|
+
padding:26px 28px;
|
|
287
|
+
border-bottom:1px solid #eaecf0;
|
|
288
|
+
">
|
|
289
|
+
|
|
290
|
+
<div style="
|
|
291
|
+
font-size:12px;
|
|
292
|
+
font-weight:700;
|
|
293
|
+
letter-spacing:1px;
|
|
294
|
+
color:#667085;
|
|
295
|
+
text-transform:uppercase;
|
|
296
|
+
margin-bottom:7px;
|
|
297
|
+
">
|
|
298
|
+
Automated Database Backup
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div style="
|
|
302
|
+
font-size:24px;
|
|
303
|
+
font-weight:700;
|
|
304
|
+
color:#101828;
|
|
305
|
+
">
|
|
306
|
+
Backup Report
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div style="
|
|
310
|
+
margin-top:6px;
|
|
311
|
+
font-size:14px;
|
|
312
|
+
color:#667085;
|
|
313
|
+
">
|
|
314
|
+
${projectName}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
</td>
|
|
318
|
+
</tr>
|
|
319
|
+
|
|
320
|
+
<!-- STATUS -->
|
|
321
|
+
<tr>
|
|
322
|
+
<td style="padding:24px 28px 10px;">
|
|
323
|
+
|
|
324
|
+
<div style="
|
|
325
|
+
background:${theme.light};
|
|
326
|
+
border:1px solid ${theme.border};
|
|
327
|
+
border-radius:12px;
|
|
328
|
+
padding:18px;
|
|
329
|
+
">
|
|
330
|
+
|
|
331
|
+
<table width="100%" cellpadding="0" cellspacing="0">
|
|
332
|
+
<tr>
|
|
333
|
+
|
|
334
|
+
<td width="48" valign="top">
|
|
335
|
+
<div style="
|
|
336
|
+
width:38px;
|
|
337
|
+
height:38px;
|
|
338
|
+
line-height:38px;
|
|
339
|
+
text-align:center;
|
|
340
|
+
background:${theme.accent};
|
|
341
|
+
color:#ffffff;
|
|
342
|
+
border-radius:50%;
|
|
343
|
+
font-size:20px;
|
|
344
|
+
font-weight:bold;
|
|
345
|
+
">
|
|
346
|
+
${theme.icon}
|
|
347
|
+
</div>
|
|
348
|
+
</td>
|
|
349
|
+
|
|
350
|
+
<td valign="middle">
|
|
351
|
+
|
|
352
|
+
<div style="
|
|
353
|
+
font-size:16px;
|
|
354
|
+
font-weight:700;
|
|
355
|
+
color:${theme.accentDark};
|
|
356
|
+
">
|
|
357
|
+
${theme.title}
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div style="
|
|
361
|
+
margin-top:4px;
|
|
362
|
+
font-size:13px;
|
|
363
|
+
line-height:1.5;
|
|
364
|
+
color:#475467;
|
|
365
|
+
">
|
|
366
|
+
${theme.description}
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
</td>
|
|
370
|
+
|
|
371
|
+
</tr>
|
|
372
|
+
</table>
|
|
373
|
+
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
</td>
|
|
377
|
+
</tr>
|
|
378
|
+
|
|
379
|
+
<!-- SUMMARY -->
|
|
380
|
+
<tr>
|
|
381
|
+
<td style="padding:14px 28px;">
|
|
382
|
+
|
|
383
|
+
<div style="
|
|
384
|
+
font-size:15px;
|
|
385
|
+
font-weight:700;
|
|
386
|
+
color:#101828;
|
|
387
|
+
margin-bottom:12px;
|
|
388
|
+
">
|
|
389
|
+
Backup Summary
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<table width="100%" cellpadding="0" cellspacing="0">
|
|
393
|
+
|
|
394
|
+
<tr>
|
|
395
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
396
|
+
Backup Date
|
|
397
|
+
</td>
|
|
398
|
+
<td align="right" style="padding:8px 0;font-size:13px;font-weight:600;">
|
|
399
|
+
${backupDate}
|
|
400
|
+
</td>
|
|
401
|
+
</tr>
|
|
402
|
+
|
|
403
|
+
<tr>
|
|
404
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
405
|
+
Start Time
|
|
406
|
+
</td>
|
|
407
|
+
<td align="right" style="padding:8px 0;font-size:13px;">
|
|
408
|
+
${startTime}
|
|
409
|
+
</td>
|
|
410
|
+
</tr>
|
|
411
|
+
|
|
412
|
+
<tr>
|
|
413
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
414
|
+
End Time
|
|
415
|
+
</td>
|
|
416
|
+
<td align="right" style="padding:8px 0;font-size:13px;">
|
|
417
|
+
${endTime}
|
|
418
|
+
</td>
|
|
419
|
+
</tr>
|
|
420
|
+
|
|
421
|
+
<tr>
|
|
422
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
423
|
+
Duration
|
|
424
|
+
</td>
|
|
425
|
+
<td align="right" style="padding:8px 0;font-size:13px;font-weight:600;">
|
|
426
|
+
${duration}
|
|
427
|
+
</td>
|
|
428
|
+
</tr>
|
|
429
|
+
|
|
430
|
+
<tr>
|
|
431
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
432
|
+
Schedule
|
|
433
|
+
</td>
|
|
434
|
+
<td align="right" style="padding:8px 0;font-size:13px;">
|
|
435
|
+
${scheduleType}
|
|
436
|
+
</td>
|
|
437
|
+
</tr>
|
|
438
|
+
|
|
439
|
+
<tr>
|
|
440
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
441
|
+
Timezone
|
|
442
|
+
</td>
|
|
443
|
+
<td align="right" style="padding:8px 0;font-size:13px;">
|
|
444
|
+
${timezone}
|
|
445
|
+
</td>
|
|
446
|
+
</tr>
|
|
447
|
+
|
|
448
|
+
<tr>
|
|
449
|
+
<td style="padding:8px 0;color:#667085;font-size:13px;">
|
|
450
|
+
Overall Status
|
|
451
|
+
</td>
|
|
452
|
+
<td align="right" style="
|
|
453
|
+
padding:8px 0;
|
|
454
|
+
font-size:13px;
|
|
455
|
+
font-weight:700;
|
|
456
|
+
color:${theme.accentDark};
|
|
457
|
+
">
|
|
458
|
+
${statusText}
|
|
459
|
+
</td>
|
|
460
|
+
</tr>
|
|
461
|
+
|
|
462
|
+
</table>
|
|
463
|
+
|
|
464
|
+
</td>
|
|
465
|
+
</tr>
|
|
466
|
+
|
|
467
|
+
<!-- COMPONENTS -->
|
|
468
|
+
<tr>
|
|
469
|
+
<td style="padding:14px 28px;">
|
|
470
|
+
|
|
471
|
+
<div style="
|
|
472
|
+
font-size:15px;
|
|
473
|
+
font-weight:700;
|
|
474
|
+
color:#101828;
|
|
475
|
+
margin-bottom:10px;
|
|
476
|
+
">
|
|
477
|
+
Backup Components
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<table width="100%" cellpadding="0" cellspacing="0">
|
|
481
|
+
<tbody>
|
|
482
|
+
${componentRows}
|
|
483
|
+
</tbody>
|
|
484
|
+
</table>
|
|
485
|
+
|
|
486
|
+
</td>
|
|
487
|
+
</tr>
|
|
488
|
+
|
|
489
|
+
<!-- FILES -->
|
|
490
|
+
<tr>
|
|
491
|
+
<td style="padding:14px 28px;">
|
|
492
|
+
|
|
493
|
+
<div style="
|
|
494
|
+
font-size:15px;
|
|
495
|
+
font-weight:700;
|
|
496
|
+
color:#101828;
|
|
497
|
+
margin-bottom:12px;
|
|
498
|
+
">
|
|
499
|
+
Generated Files
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
${filesHtml}
|
|
503
|
+
|
|
504
|
+
</td>
|
|
505
|
+
</tr>
|
|
506
|
+
|
|
507
|
+
<!-- ERRORS -->
|
|
508
|
+
${errorSection}
|
|
509
|
+
|
|
510
|
+
<!-- FOOTER -->
|
|
511
|
+
<tr>
|
|
512
|
+
<td style="
|
|
513
|
+
padding:24px 28px;
|
|
514
|
+
background:#f8fafc;
|
|
515
|
+
border-top:1px solid #eaecf0;
|
|
516
|
+
">
|
|
517
|
+
|
|
518
|
+
<div style="
|
|
519
|
+
font-size:12px;
|
|
520
|
+
line-height:1.6;
|
|
521
|
+
color:#667085;
|
|
522
|
+
text-align:center;
|
|
523
|
+
">
|
|
524
|
+
This is an automated notification from
|
|
525
|
+
<strong>MongoDB Backup Service</strong>.
|
|
526
|
+
<br>
|
|
527
|
+
Please do not reply to this email.
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
</td>
|
|
531
|
+
</tr>
|
|
532
|
+
|
|
533
|
+
</table>
|
|
534
|
+
|
|
535
|
+
</td>
|
|
536
|
+
</tr>
|
|
537
|
+
</table>
|
|
538
|
+
|
|
539
|
+
</body>
|
|
540
|
+
</html>`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Generate subject, plain-text and HTML payloads. */
|
|
544
|
+
static getBackupReport(metadata) {
|
|
545
|
+
const subject = EmailTemplates._buildSubject(metadata);
|
|
546
|
+
|
|
547
|
+
let text =
|
|
548
|
+
`Database Backup Report\n` +
|
|
549
|
+
`======================\n\n` +
|
|
550
|
+
`Project: ${metadata.projectName || ''}\n` +
|
|
551
|
+
`Backup Date: ${metadata.backupDate || ''}\n` +
|
|
552
|
+
`Start Time: ${metadata.startTime || metadata.startedAt || ''}\n` +
|
|
553
|
+
`End Time: ${metadata.endTime || metadata.completedAt || ''}\n` +
|
|
554
|
+
`Duration: ${EmailTemplates._formatDuration(metadata.durationMs || 0)}\n` +
|
|
555
|
+
`Overall Status: ${(metadata.status || '').toUpperCase()}\n\n` +
|
|
556
|
+
|
|
557
|
+
`Backup Components\n` +
|
|
558
|
+
`-----------------\n` +
|
|
559
|
+
`MongoDB Dump: ${(metadata.mongoDump?.status || 'skipped').toUpperCase()}\n` +
|
|
560
|
+
`Excel Export: ${(metadata.excel?.status || 'skipped').toUpperCase()}\n` +
|
|
561
|
+
`Metadata Generation: ${(metadata.metadata?.status || 'skipped').toUpperCase()}\n` +
|
|
562
|
+
`Retention Cleanup: ${(metadata.retention?.status || 'skipped').toUpperCase()}\n\n` +
|
|
563
|
+
|
|
564
|
+
`Backup Files\n` +
|
|
565
|
+
`------------\n` +
|
|
566
|
+
`${[
|
|
567
|
+
metadata.mongoDump?.file,
|
|
568
|
+
metadata.excel?.file,
|
|
569
|
+
metadata.metadata?.file
|
|
570
|
+
].filter(Boolean).join('\n') || 'None'}\n`;
|
|
571
|
+
|
|
572
|
+
if (metadata.status !== 'success') {
|
|
573
|
+
text +=
|
|
574
|
+
`\nError Details\n` +
|
|
575
|
+
`-------------\n` +
|
|
576
|
+
`${[
|
|
577
|
+
metadata.mongoDump,
|
|
578
|
+
metadata.excel,
|
|
579
|
+
metadata.metadata,
|
|
580
|
+
metadata.retention
|
|
581
|
+
]
|
|
582
|
+
.filter(item => item?.error)
|
|
583
|
+
.map(item => item.error)
|
|
584
|
+
.join('\n') || 'No detailed error information provided.'}\n\n` +
|
|
585
|
+
`Action Required\n` +
|
|
586
|
+
`---------------\n` +
|
|
587
|
+
`Please review the application logs and investigate the failed backup component.\n`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const html = EmailTemplates._buildHtml(metadata);
|
|
591
|
+
|
|
592
|
+
return { subject, text, html };
|
|
30
593
|
}
|
|
31
594
|
}
|
|
32
595
|
|
|
33
|
-
module.exports = EmailTemplates;
|
|
596
|
+
module.exports = EmailTemplates;
|