From 103d89f4fa2cc666ff22ab48182c5f2943fab091 Mon Sep 17 00:00:00 2001 From: Steph Ponzo Date: Tue, 24 Feb 2026 18:44:00 +0100 Subject: [PATCH] =?utf8?q?Fix:=20r=C3=A8gle=20HOLD=20+=20seuil=20confiance?= =?utf8?q?=20utilisateur=20(cahier=20des=20charges)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../modules/alerts/adapters/mysql.adapter.js | 16 ++- .../server/modules/alerts/alerts.service.js | 133 ++++++++++-------- 2 files changed, 90 insertions(+), 59 deletions(-) diff --git a/Wallette/server/modules/alerts/adapters/mysql.adapter.js b/Wallette/server/modules/alerts/adapters/mysql.adapter.js index 2f73ec2..e2bc802 100644 --- a/Wallette/server/modules/alerts/adapters/mysql.adapter.js +++ b/Wallette/server/modules/alerts/adapters/mysql.adapter.js @@ -52,12 +52,23 @@ export function createMySQLAdapter(dbConnection, options = {}) { // ===================================================== // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES // ===================================================== + /** + * Récupère les règles d'alerte pour un signal donné + * + * IMPORTANT - Cahier des charges : + * - Récupère notify_on_hold de users (pour règle HOLD) + * - Récupère min_confidence_notify de users (fallback si rule.min_confidence est NULL) + * - Utilise COALESCE pour le seuil de confiance + */ async findActiveRulesForSignal(userId, pairId) { try { const sql = ` SELECT ar.*, - u.email + u.email, + u.notify_on_hold, + u.min_confidence_notify, + COALESCE(ar.min_confidence, u.min_confidence_notify, 0.7) AS effective_min_confidence FROM ${tables.alerts} ar JOIN ${tables.users} u ON ar.user_id = u.user_id WHERE ar.enabled = 1 @@ -210,7 +221,7 @@ export function createMySQLAdapter(dbConnection, options = {}) { ruleData.enabled !== undefined ? ruleData.enabled : 1, ruleData.ruleType || 'SIGNAL_THRESHOLD', ruleData.severity || 'INFO', - ruleData.minConfidence || 0.7, + ruleData.minConfidence || null, // NULL = utiliser users.min_confidence_notify ruleData.channel || 'CONSOLE', JSON.stringify(ruleData.params || {}), ruleData.cooldownMs || 60000, @@ -341,4 +352,3 @@ export function createMySQLAdapter(dbConnection, options = {}) { }, }; } - diff --git a/Wallette/server/modules/alerts/alerts.service.js b/Wallette/server/modules/alerts/alerts.service.js index 8df13fb..7921193 100644 --- a/Wallette/server/modules/alerts/alerts.service.js +++ b/Wallette/server/modules/alerts/alerts.service.js @@ -1,11 +1,8 @@ // ========================================================= -// ALERTS SERVICE - VERSION RÉUTILISABLE +// ALERTS SERVICE // ========================================================= // RÔLE : Logique métier du système d'alertes // ========================================================= -// PRINCIPE : INJECTION DE DÉPENDANCES -// Au lieu d'importer le Repository directement, on le reçoit en paramètre -// ========================================================= import { v4 as uuidv4 } from 'uuid'; @@ -49,9 +46,9 @@ export function createAlertsService(alertsRepo, options = {}) { console.log('🔧 Service d\'alertes initialisé'); - // ───────────────────────────────────────────────────── + // ========================================================= // RETOURNER L'OBJET SERVICE - // ───────────────────────────────────────────────────── + // ========================================================= return { // ===================================================== @@ -60,35 +57,53 @@ export function createAlertsService(alertsRepo, options = {}) { /** * Détermine si une alerte doit être envoyée selon la règle * - * Cette fonction applique les RÈGLES MÉTIER : - * - Vérification de la confidence minimum + * Cette fonction applique les RÈGLES MÉTIER du cahier des charges : + * - HOLD : pas d'alerte sauf si notify_on_hold = true + * - Vérification de la confidence minimum (rule ou user) * - Vérification du cooldown (anti-spam) * - * @param {Object} rule - Règle d'alerte + * @param {Object} rule - Règle d'alerte (avec notify_on_hold et effective_min_confidence) * @param {Object} signal - Signal reçu * @returns {boolean} true si l'alerte doit être envoyée */ shouldSendAlert(rule, signal) { - console.log(`Vérification règle ${rule.rule_id}...`); + console.log(` Vérification règle ${rule.rule_id}...`); + + // ========================================================= + // CRITÈRE 0 : Règle HOLD (NOUVEAU - Cahier des charges) + // ========================================================= + // Si le signal est HOLD, on n'envoie PAS d'alerte + // SAUF si l'utilisateur a activé notify_on_hold + + if (signal.action === 'HOLD') { + if (!rule.notify_on_hold) { + console.log(` ⏸️ Signal HOLD ignoré (notify_on_hold = false)`); + return false; + } else { + console.log(` ✅ Signal HOLD accepté (notify_on_hold = true)`); + } + } - // ───────────────────────────────────────────────── + // ========================================================= // CRITÈRE 1 : Vérifier la confidence minimum - // ───────────────────────────────────────────────── - // Si le signal a une confidence trop basse, on n'envoie pas - // Exemple : rule.min_confidence = 0.80 (80%) - // signal.confidence = 0.75 (75%) - // → Ne pas envoyer (75% < 80%) - - if (signal.confidence < rule.min_confidence) { - console.log(`Confidence trop basse`); - console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`); - console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`); + // ========================================================= + // Utilise effective_min_confidence qui est : + // - rule.min_confidence si défini + // - sinon users.min_confidence_notify si défini + // - sinon 0.7 par défaut (COALESCE dans la requête SQL) + + const minConfidence = parseFloat(rule.effective_min_confidence) || parseFloat(rule.min_confidence) || 0.7; + + if (signal.confidence < minConfidence) { + console.log(` ❌ Confidence trop basse`); + console.log(` Signal : ${(signal.confidence * 100).toFixed(2)}%`); + console.log(` Minimum: ${(minConfidence * 100).toFixed(2)}%`); return false; } - // ───────────────────────────────────────────────── + // ========================================================= // CRITÈRE 2 : Vérifier le cooldown (anti-spam) - // ───────────────────────────────────────────────── + // ========================================================= // Le cooldown évite d'envoyer 100 emails en 1 minute // Exemple : cooldown_ms = 60000 (1 minute) // Dernière alerte envoyée il y a 30 secondes @@ -111,10 +126,10 @@ export function createAlertsService(alertsRepo, options = {}) { } } - // ───────────────────────────────────────────────── + // ========================================================= // TOUS LES CRITÈRES SONT OK ! - // ───────────────────────────────────────────────── - console.log(` Tous les critères sont remplis`); + // ========================================================= + console.log(` ✅ Tous les critères sont remplis`); return true; }, @@ -134,32 +149,32 @@ export function createAlertsService(alertsRepo, options = {}) { * @returns {Promise} 'SENT' ou 'FAILED' */ async sendViaChannel(rule, signal) { - console.log(`Envoi via ${rule.channel}...`); + console.log(` 📤 Envoi via ${rule.channel}...`); let status = 'SENT'; try { - // ───────────────────────────────────────────── + // ========================================================= // SWITCH selon le canal configuré - // ───────────────────────────────────────────── + // ========================================================= switch (rule.channel) { - // ── EMAIL ──────────────────────────────── + // EMAIL case 'EMAIL': // Le destinataire est l'email de l'utilisateur // On l'a récupéré via le JOIN dans la requête SQL await sendAlertEmail(rule.email, signal); break; - // ── TELEGRAM ───────────────────────────── + // TELEGRAM case 'TELEGRAM': // Le chatId est stocké dans rule.params (JSON) // Exemple de params : {"chatId": "123456789"} - const params = JSON.parse(rule.params); + const params = typeof rule.params === 'string' ? JSON.parse(rule.params) : rule.params; const chatId = params.chatId; if (!chatId) { - console.error(' Pas de chatId dans params'); + console.error(' ❌ Pas de chatId dans params'); status = 'FAILED'; break; } @@ -167,13 +182,13 @@ export function createAlertsService(alertsRepo, options = {}) { await sendTelegramAlert(chatId, signal); break; - // ── DISCORD ────────────────────────────── + // DISCORD case 'DISCORD': - const discordParams = JSON.parse(rule.params); + const discordParams = typeof rule.params === 'string' ? JSON.parse(rule.params) : rule.params; const webhookUrl = discordParams.webhookUrl; if (!webhookUrl) { - console.error(' Pas de webhookUrl dans params'); + console.error(' ❌ Pas de webhookUrl dans params'); status = 'FAILED'; break; } @@ -181,30 +196,30 @@ export function createAlertsService(alertsRepo, options = {}) { await sendDiscordAlert(webhookUrl, signal); break; - // ── CONSOLE ────────────────────────────── + // CONSOLE case 'CONSOLE': // Pas de paramètres nécessaires await sendConsoleAlert(signal); break; - // ── WEB ────────────────────────────────── + // WEB case 'WEB': // Envoyer à l'interface web de l'utilisateur await sendWebAlert(rule.user_id, signal); break; - // ── CANAL INCONNU ──────────────────────── + // CANAL INCONNU default: - console.warn(` Canal inconnu: ${rule.channel}`); + console.warn(` ⚠️ Canal inconnu: ${rule.channel}`); status = 'FAILED'; } } catch (error) { - // ───────────────────────────────────────────── + // ========================================================= // GESTION DES ERREURS - // ───────────────────────────────────────────── - console.error(` Erreur lors de l'envoi via ${rule.channel}:`); - console.error(` ${error.message}`); + // ========================================================= + console.error(` ❌ Erreur lors de l'envoi via ${rule.channel}:`); + console.error(` ${error.message}`); status = 'FAILED'; } @@ -220,7 +235,7 @@ export function createAlertsService(alertsRepo, options = {}) { * WORKFLOW : * 1. Récupérer les règles actives pour ce signal * 2. Pour chaque règle : - * a. Vérifier si alerte doit être envoyée + * a. Vérifier si alerte doit être envoyée (HOLD, confidence, cooldown) * b. Envoyer via le bon canal * c. Enregistrer dans l'historique * d. Mettre à jour le timestamp @@ -235,16 +250,17 @@ export function createAlertsService(alertsRepo, options = {}) { */ async processSignal(signal) { console.log('\n' + '═'.repeat(80)); - console.log(` TRAITEMENT DU SIGNAL`); + console.log(' TRAITEMENT DU SIGNAL'); console.log(` Action : ${signal.action}`); console.log(` Paire : ${signal.pair}`); console.log(` User : ${signal.userId}`); + console.log(` Conf. : ${(signal.confidence * 100).toFixed(2)}%`); console.log('═'.repeat(80) + '\n'); try { - // ───────────────────────────────────────────── + // ========================================================= // ÉTAPE 1 : Récupérer les règles actives - // ───────────────────────────────────────────── + // ========================================================= // Utilise le repository injecté (pas d'import hardcodé) const rules = await alertsRepo.findActiveRulesForSignal( signal.userId, @@ -259,17 +275,21 @@ export function createAlertsService(alertsRepo, options = {}) { console.log(` ${rules.length} règle(s) trouvée(s)\n`); - // ───────────────────────────────────────────── + // ========================================================= // ÉTAPE 2 : Traiter chaque règle - // ───────────────────────────────────────────── + // ========================================================= let sentCount = 0; + let skippedCount = 0; for (const rule of rules) { console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`); + console.log(`│ Seuil confiance: ${(parseFloat(rule.effective_min_confidence || rule.min_confidence || 0.7) * 100).toFixed(0)}%`); + console.log(`│ notify_on_hold: ${rule.notify_on_hold ? 'oui' : 'non'}`); // Vérifier si on doit envoyer if (!this.shouldSendAlert(rule, signal)) { - console.log(`└─ Alerte NON envoyée\n`); + console.log(`└─ ❌ Alerte NON envoyée\n`); + skippedCount++; continue; } @@ -298,19 +318,20 @@ export function createAlertsService(alertsRepo, options = {}) { if (status === 'SENT') { await alertsRepo.updateLastNotified(rule.rule_id, Date.now()); sentCount++; - console.log(`└─ Alerte envoyée avec succès\n`); + console.log(`└─ ✅ Alerte envoyée avec succès\n`); } else { - console.log(`└─ Échec de l'envoi\n`); + console.log(`└─ ❌ Échec de l'envoi\n`); } } - // ───────────────────────────────────────────── + // ========================================================= // RÉSUMÉ FINAL - // ───────────────────────────────────────────── + // ========================================================= console.log('═'.repeat(80)); - console.log(` RÉSUMÉ`); + console.log(' RÉSUMÉ'); console.log(` Règles vérifiées : ${rules.length}`); console.log(` Alertes envoyées : ${sentCount}`); + console.log(` Alertes ignorées : ${skippedCount}`); console.log('═'.repeat(80) + '\n'); } catch (error) { -- 2.50.1