Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | /**
* Service Digest Email — Envoi periodique d'un resume serveurs par email.
*
* Verifie toutes les minutes si un utilisateur doit recevoir son digest
* (quotidien ou hebdomadaire) en fonction de l'heure configuree.
*
* Contenu du digest :
* - Etat de chaque serveur (CPU, RAM, disque)
* - Uptime des monitors
* - Backups effectues sur la periode
* - Alertes declenchees sur la periode
*
* @module services/digestScheduler
*/
const { pool: db } = require('../db');
const { sendEmail } = require('./email');
const { getPlanLimits } = require('../config/plans');
const { getUptimeStats, getBackupStats } = require('../helpers/schedulerStats');
// Intervalle de verification (toutes les minutes)
const CHECK_INTERVAL_MS = 60 * 1000;
let checkTimer = null;
/**
* Demarre le scheduler de digest.
* Verifie chaque minute s'il y a des digests a envoyer.
*/
function startDigestScheduler() {
if (process.env.NODE_ENV === 'test') return;
// Premiere verification apres 60s
setTimeout(checkAndSendDigests, 60 * 1000);
checkTimer = setInterval(checkAndSendDigests, CHECK_INTERVAL_MS);
console.log('[digest] Scheduler demarre — verification toutes les minutes');
}
/**
* Arrete le scheduler.
*/
function stopDigestScheduler() {
if (checkTimer) {
clearInterval(checkTimer);
checkTimer = null;
}
}
/**
* Verifie quels utilisateurs doivent recevoir leur digest maintenant.
*/
async function checkAndSendDigests() {
try {
const now = new Date();
const currentHour = now.getUTCHours();
const currentMinute = now.getUTCMinutes();
const currentDay = now.getUTCDay(); // 0 = dimanche
// Ne traiter qu'au debut de l'heure (minute 0)
if (currentMinute !== 0) return;
// Charger les utilisateurs qui ont le digest active
const { rows: users } = await db.query(
`SELECT id, email, plan, digest_frequency, digest_hour, digest_last_sent
FROM users
WHERE digest_enabled = TRUE AND email_verified = TRUE`,
);
for (const user of users) {
try {
const shouldSend = shouldSendDigest(user, currentHour, currentDay);
if (!shouldSend) continue;
// Verifier que le plan autorise le digest
const limits = await getPlanLimits(user.plan || 'free');
if (!limits.email_digest) continue;
await sendDigestForUser(user);
// Mettre a jour la date d'envoi
await db.query(
'UPDATE users SET digest_last_sent = NOW() WHERE id = $1',
[user.id],
);
} catch (err) {
console.error(`[digest] Erreur pour ${user.email}:`, err.message);
}
}
} catch (err) {
console.error('[digest] Erreur verification:', err.message);
}
}
/**
* Determine si un digest doit etre envoye maintenant.
*
* @param {Object} user
* @param {number} currentHour — heure UTC
* @param {number} currentDay — jour de la semaine (0=dimanche)
* @returns {boolean}
*/
function shouldSendDigest(user, currentHour, currentDay) {
// L'heure UTC doit correspondre
if (user.digest_hour !== currentHour) return false;
// Verifier qu'on n'a pas deja envoye recemment
if (user.digest_last_sent) {
const lastSent = new Date(user.digest_last_sent);
const hoursSince = (Date.now() - lastSent.getTime()) / (1000 * 60 * 60);
if (user.digest_frequency === 'daily' && hoursSince < 20) return false;
if (user.digest_frequency === 'weekly' && hoursSince < 144) return false;
}
// Hebdomadaire = seulement le lundi (jour 1)
if (user.digest_frequency === 'weekly' && currentDay !== 1) return false;
return true;
}
/**
* Genere et envoie le digest pour un utilisateur.
*
* @param {Object} user — { id, email, plan }
*/
async function sendDigestForUser(user) {
const periodLabel = user.digest_frequency === 'daily' ? 'les 24 dernieres heures' : 'les 7 derniers jours';
const intervalSql = user.digest_frequency === 'daily' ? '24 hours' : '7 days';
// Collecter les donnees du digest
const [servers, uptimeStats, backupStats, alertStats] = await Promise.all([
getServersStatus(user.id),
getUptimeStats(user.id, intervalSql),
getBackupStats(user.id, intervalSql),
getAlertStats(user.id, intervalSql),
]);
if (servers.length === 0) return; // Pas de serveur, pas de digest
const html = buildDigestHtml({
email: user.email,
frequency: user.digest_frequency,
periodLabel,
servers,
uptimeStats,
backupStats,
alertStats,
});
await sendEmail({
to: user.email,
subject: `Resume ${user.digest_frequency === 'daily' ? 'quotidien' : 'hebdomadaire'} — IliaCloud`,
html,
});
console.log(`[digest] Envoye a ${user.email} (${user.digest_frequency})`);
}
// ─── Collecte des donnees ────────────────────────────────────────────────────
/**
* Recupere le dernier etat de chaque serveur de l'utilisateur.
*/
async function getServersStatus(userId) {
const { rows } = await db.query(
`SELECT s.id, s.name, s.host,
mh.cpu_percent, mh.mem_percent, mh.disk_percent, mh.load_1m
FROM servers s
LEFT JOIN LATERAL (
SELECT cpu_percent, mem_percent, disk_percent, load_1m
FROM metrics_history
WHERE server_id = s.id
ORDER BY created_at DESC LIMIT 1
) mh ON true
WHERE s.user_id = $1
ORDER BY s.name`,
[userId],
);
return rows;
}
/**
* Nombre d'alertes declenchees sur la periode.
*/
async function getAlertStats(userId, interval) {
const { rows } = await db.query(
`SELECT COUNT(*) AS total
FROM alert_history
WHERE user_id = $1 AND created_at > NOW() - INTERVAL '${interval}'`,
[userId],
);
return {
total: Number(rows[0]?.total || 0),
alerts: Number(rows[0]?.total || 0),
recoveries: 0,
};
}
// ─── Template HTML ───────────────────────────────────────────────────────────
/**
* Construit le contenu HTML du digest.
*/
function buildDigestHtml({ email, frequency, periodLabel, servers, uptimeStats, backupStats, alertStats }) {
const frequencyLabel = frequency === 'daily' ? 'Quotidien' : 'Hebdomadaire';
// Generer les lignes serveurs
const serverRows = servers.map((s) => {
const cpu = s.cpu_percent == null ? '—' : `${s.cpu_percent}%`;
const ram = s.mem_percent == null ? '—' : `${s.mem_percent}%`;
const disk = s.disk_percent == null ? '—' : `${s.disk_percent}%`;
const load = s.load_1m == null ? '—' : s.load_1m.toFixed(2);
const statusColor = getStatusColor(s);
return `
<tr>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb;">
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${statusColor}; margin-right: 8px;"></span>
<strong>${escapeHtml(s.name)}</strong>
<span style="color: #6b7280; font-size: 12px; margin-left: 6px;">${escapeHtml(s.host)}</span>
</td>
<td style="padding: 10px 8px; border-bottom: 1px solid #e5e7eb; text-align: center;">${cpu}</td>
<td style="padding: 10px 8px; border-bottom: 1px solid #e5e7eb; text-align: center;">${ram}</td>
<td style="padding: 10px 8px; border-bottom: 1px solid #e5e7eb; text-align: center;">${disk}</td>
<td style="padding: 10px 8px; border-bottom: 1px solid #e5e7eb; text-align: center;">${load}</td>
</tr>`;
}).join('');
// Generer les lignes uptime
let uptimeSection = '';
if (uptimeStats.length > 0) {
const uptimeRows = uptimeStats.map((u) => {
const pct = u.percent == null ? '—' : `${u.percent}%`;
let color = '#ef4444';
if (u.percent >= 99) color = '#22c55e';
else if (u.percent >= 95) color = '#f59e0b';
return `
<tr>
<td style="padding: 6px 12px; border-bottom: 1px solid #e5e7eb; font-size: 13px;">${escapeHtml(u.url)}</td>
<td style="padding: 6px 8px; border-bottom: 1px solid #e5e7eb; text-align: center;">
<span style="color: ${color}; font-weight: 600;">${pct}</span>
</td>
</tr>`;
}).join('');
uptimeSection = `
<h3 style="margin: 24px 0 12px; font-size: 16px; color: #1f2937;">Uptime</h3>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb;">
<thead><tr style="background: #f9fafb;">
<th style="padding: 8px 12px; text-align: left; font-size: 12px; color: #6b7280;">URL</th>
<th style="padding: 8px; text-align: center; font-size: 12px; color: #6b7280;">Dispo.</th>
</tr></thead>
<tbody>${uptimeRows}</tbody>
</table>`;
}
return `
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body style="margin: 0; padding: 0; background-color: #f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f7;">
<tr><td align="center" style="padding: 40px 20px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background: #ffffff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<!-- Header -->
<tr><td style="padding: 32px 32px 16px; text-align: center;">
<h1 style="margin: 0 0 4px; font-size: 22px; color: #1f2937;">Resume ${frequencyLabel}</h1>
<p style="margin: 0; color: #6b7280; font-size: 13px;">${periodLabel}</p>
</td></tr>
<!-- Stats rapides -->
<tr><td style="padding: 8px 32px 16px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: center; padding: 12px; background: #f0fdf4; border-radius: 8px; width: 33%;">
<div style="font-size: 24px; font-weight: 700; color: #16a34a;">${servers.length}</div>
<div style="font-size: 11px; color: #6b7280;">Serveur${servers.length > 1 ? 's' : ''}</div>
</td>
<td style="width: 8px;"></td>
<td style="text-align: center; padding: 12px; background: ${backupStats.errors > 0 ? '#fef2f2' : '#f0fdf4'}; border-radius: 8px; width: 33%;">
<div style="font-size: 24px; font-weight: 700; color: ${backupStats.errors > 0 ? '#dc2626' : '#16a34a'};">${backupStats.total}</div>
<div style="font-size: 11px; color: #6b7280;">Backup${backupStats.total > 1 ? 's' : ''}</div>
</td>
<td style="width: 8px;"></td>
<td style="text-align: center; padding: 12px; background: ${alertStats.alerts > 0 ? '#fef2f2' : '#f0fdf4'}; border-radius: 8px; width: 33%;">
<div style="font-size: 24px; font-weight: 700; color: ${alertStats.alerts > 0 ? '#dc2626' : '#16a34a'};">${alertStats.alerts}</div>
<div style="font-size: 11px; color: #6b7280;">Alerte${alertStats.alerts > 1 ? 's' : ''}</div>
</td>
</tr>
</table>
</td></tr>
<!-- Serveurs -->
<tr><td style="padding: 0 32px 16px;">
<h3 style="margin: 0 0 12px; font-size: 16px; color: #1f2937;">Serveurs</h3>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb;">
<thead><tr style="background: #f9fafb;">
<th style="padding: 8px 12px; text-align: left; font-size: 12px; color: #6b7280;">Serveur</th>
<th style="padding: 8px; text-align: center; font-size: 12px; color: #6b7280;">CPU</th>
<th style="padding: 8px; text-align: center; font-size: 12px; color: #6b7280;">RAM</th>
<th style="padding: 8px; text-align: center; font-size: 12px; color: #6b7280;">Disque</th>
<th style="padding: 8px; text-align: center; font-size: 12px; color: #6b7280;">Load</th>
</tr></thead>
<tbody>${serverRows}</tbody>
</table>
</td></tr>
<!-- Uptime -->
${uptimeSection ? `<tr><td style="padding: 0 32px 16px;">${uptimeSection}</td></tr>` : ''}
<!-- Footer -->
<tr><td style="padding: 24px 32px; text-align: center; border-top: 1px solid #e5e7eb;">
<p style="margin: 0; font-size: 12px; color: #9ca3af;">
Ce resume est envoye ${frequency === 'daily' ? 'tous les jours' : 'chaque lundi'} a ${escapeHtml(email)}.
<br>Gerez vos preferences dans Parametres > Notifications.
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>`;
}
/**
* Determine la couleur de statut d'un serveur.
*/
function getStatusColor(server) {
if (server.cpu_percent == null) return '#9ca3af'; // gris — pas de donnees
if (server.cpu_percent > 90 || server.mem_percent > 90 || server.disk_percent > 95) return '#ef4444'; // rouge
if (server.cpu_percent > 70 || server.mem_percent > 70 || server.disk_percent > 85) return '#f59e0b'; // orange
return '#22c55e'; // vert
}
/**
* Echappe le HTML pour eviter les XSS dans les emails.
*/
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
}
module.exports = {
startDigestScheduler,
stopDigestScheduler,
checkAndSendDigests,
shouldSendDigest,
sendDigestForUser,
getServersStatus,
getUptimeStats,
getBackupStats,
getAlertStats,
buildDigestHtml,
escapeHtml,
getStatusColor,
};
|