]> git.digitality.be Git - pdw25-26/commitdiff
Alert module version 0.2.1 - Clean routes
authorponzost <blackubu@BLack-Fix.>
Tue, 24 Feb 2026 12:14:11 +0000 (13:14 +0100)
committerponzost <blackubu@BLack-Fix.>
Tue, 24 Feb 2026 12:14:11 +0000 (13:14 +0100)
14 files changed:
Wallette/server/modules/alerts/adapters/mysql.adapter.js
Wallette/server/modules/alerts/alerts.controller.js
Wallette/server/modules/alerts/alerts.repo.js
Wallette/server/modules/alerts/alerts.router.js
Wallette/server/modules/alerts/alerts.service.js
Wallette/server/modules/alerts/channels/console.js
Wallette/server/modules/alerts/channels/discord.js
Wallette/server/modules/alerts/channels/mailer.js
Wallette/server/modules/alerts/channels/telegram.js
Wallette/server/modules/alerts/channels/web.js
Wallette/server/modules/alerts/index.js
Wallette/server/modules/alerts/socketManager.js
Wallette/server/modules/alerts/test-alerts.js
Wallette/server/modules/init-alerts.js

index 7236df1b99ada81a7ff5f05fa7fbaaab6bc6a848..830efbd10a2b36ab7b7dd1acf9f396ba45c59ce4 100644 (file)
-// =========================================================
-// MYSQL ADAPTER - Couche d'accès MySQL
-// =========================================================
-// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB
-// =========================================================
-// Ce fichier isole le SQL du reste du module.
-// Pour utiliser MongoDB, créer un mongo.adapter.js
-// qui implémente les mêmes méthodes.
-// =========================================================
-
-import { v4 as uuidv4 } from 'uuid';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un adapter MySQL
-// =========================================================
-/**
- * Crée un adapter MySQL pour le module d'alertes
- *
- * @param {Object} dbConnection - Connexion MySQL (mysql2/promise)
- * @param {Object} options - Configuration des noms de tables
- * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules')
- * @param {string} options.usersTable - Nom de la table users (défaut: 'users')
- * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events')
- *
- * @returns {Object} Adapter avec toutes les méthodes d'accès DB
- *
- * @example
- * import db from './config/db.js';
- * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
- *
- * const adapter = createMySQLAdapter(db);
- * const rules = await adapter.findActiveRulesForSignal('user-123', 1);
- */
-export function createMySQLAdapter(dbConnection, options = {}) {
-
-    // =========================================================
-    // CONFIGURATION DES NOMS DE TABLES
-    // =========================================================
-    const tables = {
-        alerts: options.alertsTable || 'alert_rules',
-        users: options.usersTable || 'users',
-        events: options.eventsTable || 'alert_events'
-    };
-
-    console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`);
-
-    // =========================================================
-    // RETOURNER L'OBJET ADAPTER
-    // =========================================================
-    return {
-
-        // =====================================================
-        // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
-        // =====================================================
-        async findActiveRulesForSignal(userId, pairId) {
-            try {
-                const sql = `
-                    SELECT
-                        ar.*,
-                        u.email
-                    FROM ${tables.alerts} ar
-                    JOIN ${tables.users} u ON ar.user_id = u.user_id
-                    WHERE ar.enabled = 1
-                        AND ar.user_id = ?
-                        AND (ar.pair_id = ? OR ar.pair_id IS NULL)
-                    ORDER BY ar.severity DESC
-                `;
-
-                const [rows] = await dbConnection.execute(sql, [userId, pairId]);
-
-                console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`);
-                return rows;
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
-        // =====================================================
-        async saveAlertEvent(eventData) {
-            try {
-                const sql = `
-                    INSERT INTO ${tables.events} (
-                        alert_event_id,
-                        rule_id,
-                        timestamp_ms,
-                        severity,
-                        channel,
-                        send_status,
-                        title,
-                        message,
-                        signal_action,
-                        confidence,
-                        correlation_id,
-                        created_at_ms
-                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-                `;
-
-                const values = [
-                    eventData.alert_event_id || uuidv4(),
-                    eventData.rule_id,
-                    eventData.timestamp_ms || Date.now(),
-                    eventData.severity,
-                    eventData.channel,
-                    eventData.send_status,
-                    eventData.title,
-                    eventData.message,
-                    eventData.signal_action,
-                    eventData.confidence,
-                    eventData.correlation_id || null,
-                    Date.now()
-                ];
-
-                await dbConnection.execute(sql, values);
-                console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`);
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans saveAlertEvent:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
-        // =====================================================
-        async updateLastNotified(ruleId, timestamp) {
-            try {
-                const sql = `
-                    UPDATE ${tables.alerts}
-                    SET last_notified_at_ms = ?,
-                        updated_at_ms = ?
-                    WHERE rule_id = ?
-                `;
-
-                await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]);
-                console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`);
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans updateLastNotified:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
-        // =====================================================
-        async getAlertHistory(userId, limit = 50) {
-            try {
-                const sql = `
-                    SELECT
-                        ae.*,
-                        ar.channel,
-                        ar.rule_type
-                    FROM ${tables.events} ae
-                    JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id
-                    WHERE ar.user_id = ?
-                    ORDER BY ae.timestamp_ms DESC
-                    LIMIT ?
-                `;
-
-                const [rows] = await dbConnection.execute(sql, [userId, limit]);
-                return rows;
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans getAlertHistory:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
-        // =====================================================
-        /**
-         * Crée une nouvelle règle d'alerte
-         * @param {Object} ruleData - Données de la règle
-         * @returns {Promise<Object>} Règle créée avec son ID
-         */
-        async createRule(ruleData) {
-            try {
-                const ruleId = ruleData.ruleId || uuidv4();
-                const now = Date.now();
-
-                const sql = `
-                    INSERT INTO ${tables.alerts} (
-                        rule_id,
-                        user_id,
-                        pair_id,
-                        enabled,
-                        rule_type,
-                        severity,
-                        min_confidence,
-                        channel,
-                        params,
-                        cooldown_ms,
-                        created_at_ms,
-                        updated_at_ms
-                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-                `;
-
-                const values = [
-                    ruleId,
-                    ruleData.userId,
-                    ruleData.pairId || null,
-                    ruleData.enabled !== undefined ? ruleData.enabled : 1,
-                    ruleData.ruleType || 'SIGNAL_THRESHOLD',
-                    ruleData.severity || 'INFO',
-                    ruleData.minConfidence || 0.7,
-                    ruleData.channel || 'CONSOLE',
-                    JSON.stringify(ruleData.params || {}),
-                    ruleData.cooldownMs || 60000,
-                    now,
-                    now
-                ];
-
-                await dbConnection.execute(sql, values);
-                console.log(`[MySQL] Règle créée : ${ruleId}`);
-
-                return { ruleId, ...ruleData, createdAt: now };
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans createRule:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
-        // =====================================================
-        /**
-         * Met à jour une règle existante
-         * @param {string} ruleId - ID de la règle
-         * @param {Object} updates - Champs à mettre à jour
-         * @returns {Promise<boolean>} true si modifié
-         */
-        async updateRule(ruleId, updates) {
-            try {
-                // Construire la requête dynamiquement
-                const fields = [];
-                const values = [];
-
-                if (updates.minConfidence !== undefined) {
-                    fields.push('min_confidence = ?');
-                    values.push(updates.minConfidence);
-                }
-                if (updates.enabled !== undefined) {
-                    fields.push('enabled = ?');
-                    values.push(updates.enabled);
-                }
-                if (updates.channel !== undefined) {
-                    fields.push('channel = ?');
-                    values.push(updates.channel);
-                }
-                if (updates.severity !== undefined) {
-                    fields.push('severity = ?');
-                    values.push(updates.severity);
-                }
-                if (updates.cooldownMs !== undefined) {
-                    fields.push('cooldown_ms = ?');
-                    values.push(updates.cooldownMs);
-                }
-                if (updates.pairId !== undefined) {
-                    fields.push('pair_id = ?');
-                    values.push(updates.pairId);
-                }
-
-                // Toujours mettre à jour le timestamp
-                fields.push('updated_at_ms = ?');
-                values.push(Date.now());
-
-                // Ajouter le ruleId à la fin pour le WHERE
-                values.push(ruleId);
-
-                const sql = `
-                    UPDATE ${tables.alerts}
-                    SET ${fields.join(', ')}
-                    WHERE rule_id = ?
-                `;
-
-                const [result] = await dbConnection.execute(sql, values);
-                console.log(`[MySQL] Règle ${ruleId} mise à jour`);
-
-                return result.affectedRows > 0;
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans updateRule:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
-        // =====================================================
-        /**
-         * Supprime une règle
-         * @param {string} ruleId - ID de la règle
-         * @returns {Promise<boolean>} true si supprimé
-         */
-        async deleteRule(ruleId) {
-            try {
-                const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`;
-
-                const [result] = await dbConnection.execute(sql, [ruleId]);
-                console.log(`[MySQL] Règle ${ruleId} supprimée`);
-
-                return result.affectedRows > 0;
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans deleteRule:', error);
-                throw error;
-            }
-        },
-
-        // =====================================================
-        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
-        // =====================================================
-        /**
-         * Récupère une règle par son ID
-         * @param {string} ruleId - ID de la règle
-         * @returns {Promise<Object|null>} La règle ou null
-         */
-        async findRuleById(ruleId) {
-            try {
-                const sql = `
-                    SELECT * FROM ${tables.alerts}
-                    WHERE rule_id = ?
-                `;
-
-                const [rows] = await dbConnection.execute(sql, [ruleId]);
-                return rows.length > 0 ? rows[0] : null;
-
-            } catch (error) {
-                console.error('[MySQL] Erreur dans findRuleById:', error);
-                throw error;
-            }
-        },
-    };
-}
-
+// =========================================================\r
+// MYSQL ADAPTER - Couche d'accès MySQL\r
+// =========================================================\r
+// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB\r
+// =========================================================\r
+// Ce fichier isole le SQL du reste du module.\r
+// Pour utiliser MongoDB, créer un mongo.adapter.js\r
+// qui implémente les mêmes méthodes.\r
+// =========================================================\r
+\r
+import { v4 as uuidv4 } from 'uuid';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un adapter MySQL\r
+// =========================================================\r
+/**\r
+ * Crée un adapter MySQL pour le module d'alertes\r
+ *\r
+ * @param {Object} dbConnection - Connexion MySQL (mysql2/promise)\r
+ * @param {Object} options - Configuration des noms de tables\r
+ * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules')\r
+ * @param {string} options.usersTable - Nom de la table users (défaut: 'users')\r
+ * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events')\r
+ *\r
+ * @returns {Object} Adapter avec toutes les méthodes d'accès DB\r
+ *\r
+ * @example\r
+ * import db from './config/db.js';\r
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+ *\r
+ * const adapter = createMySQLAdapter(db);\r
+ * const rules = await adapter.findActiveRulesForSignal('user-123', 1);\r
+ */\r
+export function createMySQLAdapter(dbConnection, options = {}) {\r
+\r
+    // =========================================================\r
+    // CONFIGURATION DES NOMS DE TABLES\r
+    // =========================================================\r
+    const tables = {\r
+        alerts: options.alertsTable || 'alert_rules',\r
+        users: options.usersTable || 'users',\r
+        events: options.eventsTable || 'alert_events'\r
+    };\r
+\r
+    console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`);\r
+\r
+    // =========================================================\r
+    // RETOURNER L'OBJET ADAPTER\r
+    // =========================================================\r
+    return {\r
+\r
+        // =====================================================\r
+        // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES\r
+        // =====================================================\r
+        async findActiveRulesForSignal(userId, pairId) {\r
+            try {\r
+                const sql = `\r
+                    SELECT\r
+                        ar.*,\r
+                        u.email\r
+                    FROM ${tables.alerts} ar\r
+                    JOIN ${tables.users} u ON ar.user_id = u.user_id\r
+                    WHERE ar.enabled = 1\r
+                        AND ar.user_id = ?\r
+                        AND (ar.pair_id = ? OR ar.pair_id IS NULL)\r
+                    ORDER BY ar.severity DESC\r
+                `;\r
+\r
+                const [rows] = await dbConnection.execute(sql, [userId, pairId]);\r
+\r
+                console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`);\r
+                return rows;\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE\r
+        // =====================================================\r
+        async saveAlertEvent(eventData) {\r
+            try {\r
+                const sql = `\r
+                    INSERT INTO ${tables.events} (\r
+                        alert_event_id,\r
+                        rule_id,\r
+                        timestamp_ms,\r
+                        severity,\r
+                        channel,\r
+                        send_status,\r
+                        title,\r
+                        message,\r
+                        signal_action,\r
+                        confidence,\r
+                        correlation_id,\r
+                        created_at_ms\r
+                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r
+                `;\r
+\r
+                const values = [\r
+                    eventData.alert_event_id || uuidv4(),\r
+                    eventData.rule_id,\r
+                    eventData.timestamp_ms || Date.now(),\r
+                    eventData.severity,\r
+                    eventData.channel,\r
+                    eventData.send_status,\r
+                    eventData.title,\r
+                    eventData.message,\r
+                    eventData.signal_action,\r
+                    eventData.confidence,\r
+                    eventData.correlation_id || null,\r
+                    Date.now()\r
+                ];\r
+\r
+                await dbConnection.execute(sql, values);\r
+                console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`);\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans saveAlertEvent:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION\r
+        // =====================================================\r
+        async updateLastNotified(ruleId, timestamp) {\r
+            try {\r
+                const sql = `\r
+                    UPDATE ${tables.alerts}\r
+                    SET last_notified_at_ms = ?,\r
+                        updated_at_ms = ?\r
+                    WHERE rule_id = ?\r
+                `;\r
+\r
+                await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]);\r
+                console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`);\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans updateLastNotified:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES\r
+        // =====================================================\r
+        async getAlertHistory(userId, limit = 50) {\r
+            try {\r
+                const sql = `\r
+                    SELECT\r
+                        ae.*,\r
+                        ar.channel,\r
+                        ar.rule_type\r
+                    FROM ${tables.events} ae\r
+                    JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id\r
+                    WHERE ar.user_id = ?\r
+                    ORDER BY ae.timestamp_ms DESC\r
+                    LIMIT ?\r
+                `;\r
+\r
+                const [rows] = await dbConnection.execute(sql, [userId, limit]);\r
+                return rows;\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans getAlertHistory:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)\r
+        // =====================================================\r
+        /**\r
+         * Crée une nouvelle règle d'alerte\r
+         * @param {Object} ruleData - Données de la règle\r
+         * @returns {Promise<Object>} Règle créée avec son ID\r
+         */\r
+        async createRule(ruleData) {\r
+            try {\r
+                const ruleId = ruleData.ruleId || uuidv4();\r
+                const now = Date.now();\r
+\r
+                const sql = `\r
+                    INSERT INTO ${tables.alerts} (\r
+                        rule_id,\r
+                        user_id,\r
+                        pair_id,\r
+                        enabled,\r
+                        rule_type,\r
+                        severity,\r
+                        min_confidence,\r
+                        channel,\r
+                        params,\r
+                        cooldown_ms,\r
+                        created_at_ms,\r
+                        updated_at_ms\r
+                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r
+                `;\r
+\r
+                const values = [\r
+                    ruleId,\r
+                    ruleData.userId,\r
+                    ruleData.pairId || null,\r
+                    ruleData.enabled !== undefined ? ruleData.enabled : 1,\r
+                    ruleData.ruleType || 'SIGNAL_THRESHOLD',\r
+                    ruleData.severity || 'INFO',\r
+                    ruleData.minConfidence || 0.7,\r
+                    ruleData.channel || 'CONSOLE',\r
+                    JSON.stringify(ruleData.params || {}),\r
+                    ruleData.cooldownMs || 60000,\r
+                    now,\r
+                    now\r
+                ];\r
+\r
+                await dbConnection.execute(sql, values);\r
+                console.log(`[MySQL] Règle créée : ${ruleId}`);\r
+\r
+                return { ruleId, ...ruleData, createdAt: now };\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans createRule:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)\r
+        // =====================================================\r
+        /**\r
+         * Met à jour une règle existante\r
+         * @param {string} ruleId - ID de la règle\r
+         * @param {Object} updates - Champs à mettre à jour\r
+         * @returns {Promise<boolean>} true si modifié\r
+         */\r
+        async updateRule(ruleId, updates) {\r
+            try {\r
+                // Construire la requête dynamiquement\r
+                const fields = [];\r
+                const values = [];\r
+\r
+                if (updates.minConfidence !== undefined) {\r
+                    fields.push('min_confidence = ?');\r
+                    values.push(updates.minConfidence);\r
+                }\r
+                if (updates.enabled !== undefined) {\r
+                    fields.push('enabled = ?');\r
+                    values.push(updates.enabled);\r
+                }\r
+                if (updates.channel !== undefined) {\r
+                    fields.push('channel = ?');\r
+                    values.push(updates.channel);\r
+                }\r
+                if (updates.severity !== undefined) {\r
+                    fields.push('severity = ?');\r
+                    values.push(updates.severity);\r
+                }\r
+                if (updates.cooldownMs !== undefined) {\r
+                    fields.push('cooldown_ms = ?');\r
+                    values.push(updates.cooldownMs);\r
+                }\r
+                if (updates.pairId !== undefined) {\r
+                    fields.push('pair_id = ?');\r
+                    values.push(updates.pairId);\r
+                }\r
+\r
+                // Toujours mettre à jour le timestamp\r
+                fields.push('updated_at_ms = ?');\r
+                values.push(Date.now());\r
+\r
+                // Ajouter le ruleId à la fin pour le WHERE\r
+                values.push(ruleId);\r
+\r
+                const sql = `\r
+                    UPDATE ${tables.alerts}\r
+                    SET ${fields.join(', ')}\r
+                    WHERE rule_id = ?\r
+                `;\r
+\r
+                const [result] = await dbConnection.execute(sql, values);\r
+                console.log(`[MySQL] Règle ${ruleId} mise à jour`);\r
+\r
+                return result.affectedRows > 0;\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans updateRule:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)\r
+        // =====================================================\r
+        /**\r
+         * Supprime une règle\r
+         * @param {string} ruleId - ID de la règle\r
+         * @returns {Promise<boolean>} true si supprimé\r
+         */\r
+        async deleteRule(ruleId) {\r
+            try {\r
+                const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`;\r
+\r
+                const [result] = await dbConnection.execute(sql, [ruleId]);\r
+                console.log(`[MySQL] Règle ${ruleId} supprimée`);\r
+\r
+                return result.affectedRows > 0;\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans deleteRule:', error);\r
+                throw error;\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)\r
+        // =====================================================\r
+        /**\r
+         * Récupère une règle par son ID\r
+         * @param {string} ruleId - ID de la règle\r
+         * @returns {Promise<Object|null>} La règle ou null\r
+         */\r
+        async findRuleById(ruleId) {\r
+            try {\r
+                const sql = `\r
+                    SELECT * FROM ${tables.alerts}\r
+                    WHERE rule_id = ?\r
+                `;\r
+\r
+                const [rows] = await dbConnection.execute(sql, [ruleId]);\r
+                return rows.length > 0 ? rows[0] : null;\r
+\r
+            } catch (error) {\r
+                console.error('[MySQL] Erreur dans findRuleById:', error);\r
+                throw error;\r
+            }\r
+        },\r
+    };\r
+}\r
+\r
index ea02247c55e43cff1d20da4300001e939371be69..a15378779e4e5ac3e9d7af54c3a2f5f1c3407469 100644 (file)
-// =========================================================
-// ALERTS CONTROLLER - VERSION RÉUTILISABLE
-// =========================================================
-// RÔLE : Gestion des requêtes et réponses HTTP
-// =========================================================
-
-
-/**
- * Crée un controller d'alertes réutilisable
- *
- * @param {Object} alertsService - Service d'alertes (créé par createAlertsService)
- * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes)
- *
- * @returns {Object} Controller avec toutes les méthodes
- */
-export function createAlertsController(alertsService, alertsRepo) {
-
-    console.log("Controller d'alertes initialisé");
-
-    return {
-
-        // =====================================================
-        // POST /api/alerts/process-signal
-        // =====================================================
-        /**
-         * Endpoint pour traiter un signal crypto
-         *
-         * Appelé par le module Strategy (Sacha) quand il
-         * génère un signal BUY/SELL/HOLD
-         *
-         * Body attendu :
-         * {
-         *   "userId": "uuid",
-         *   "pairId": 1,
-         *   "pair": "BTC/EUR",
-         *   "action": "BUY",
-         *   "confidence": 0.87,
-         *   "criticality": "WARNING",
-         *   "reason": "Indicators show...",
-         *   "priceAtSignal": 45000.50
-         * }
-         */
-        async processSignal(req, res) {
-            try {
-                // ==================================================================================
-                // 1. RÉCUPÉRER LES DONNÉES DU BODY
-                // ==================================================================================
-                const signal = req.body;
-
-                // ==================================================================================
-                // 2. VALIDATION DES DONNÉES
-                // ==================================================================================
-                // Vérifier que tous les champs obligatoires sont présents
-                const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality'];
-                /**
-                 *  on remplit le tableau des champs manquant ou faux du contenu de signal
-                 */
-                const missingFields = requiredFields.filter(field => !signal[field]);
-                // dans ce cas on retourne une erreur et le noms des champs en cause
-                if (missingFields.length > 0) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'Champs manquants',
-                        missingFields: missingFields
-                    });
-                }
-
-                // Valider le format de action
-                const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS'];
-                if (!validActions.includes(signal.action)) {
-                    return res.status(400).json({
-                        success: false,
-                        error: `Action invalide. Doit être: ${validActions.join(', ')}`
-                    });
-                }
-
-                // Valider le format de criticality
-                const validCriticalities = ['CRITICAL', 'WARNING', 'INFO'];
-                if (!validCriticalities.includes(signal.criticality)) {
-                    return res.status(400).json({
-                        success: false,
-                        error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}`
-                    });
-                }
-
-                // Valider la confidence (doit être entre 0 et 1)
-                if (signal.confidence < 0 || signal.confidence > 1) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'Confidence doit être entre 0 et 1'
-                    });
-                }
-
-                // =================================================================================
-                // 3. APPELER LE SERVICE (injecté)
-                // =================================================================================
-                // Le service contient toute la logique
-                await alertsService.processSignal(signal);
-
-                // =================================================================================
-                // 4. RÉPONDRE AU CLIENT
-                // =================================================================================
-                res.status(200).json({
-                    success: true,
-                    message: 'Signal traité avec succès',
-                    signal: {
-                        action: signal.action,
-                        pair: signal.pair,
-                        confidence: signal.confidence
-                    }
-                });
-
-            } catch (error) {
-                // =================================================================================
-                // GESTION DES ERREURS
-                // =================================================================================
-                console.error(' Erreur dans processSignal controller:', error);
-
-                res.status(500).json({
-                    success: false,
-                    error: 'Erreur serveur lors du traitement du signal',
-                    details: process.env.NODE_ENV === 'development' ? error.message : undefined
-                });
-            }
-        },
-
-        // =====================================================
-        // GET /api/alerts/rules/:userId
-        // =====================================================
-        /**
-         * Récupère toutes les règles d'alerte d'un utilisateur
-         *
-         * Utile pour afficher la configuration dans l'interface
-         */
-        async getRules(req, res) {
-            try {
-                const { userId } = req.params;
-
-                // Validation
-                if (!userId) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'userId manquant'
-                    });
-                }
-
-                // Récupérer les règles (via repo injecté)
-                const rules = await alertsRepo.findActiveRulesForSignal(userId, null);
-
-                res.status(200).json({
-                    success: true,
-                    count: rules.length,
-                    rules: rules
-                });
-
-            } catch (error) {
-                console.error('Erreur dans getRules controller:', error);
-
-                res.status(500).json({
-                    success: false,
-                    error: 'Erreur serveur'
-                });
-            }
-        },
-
-        // =====================================================
-        // GET /api/alerts/history/:userId
-        // =====================================================
-        /**
-         * Récupère l'historique des alertes d'un utilisateur
-         */
-        async getHistory(req, res) {
-            try {
-                const { userId } = req.params;
-                const limit = parseInt(req.query.limit) || 50;
-
-                if (!userId) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'userId manquant'
-                    });
-                }
-
-                // Récupérer l'historique (via repo injecté)
-                const history = await alertsRepo.getAlertHistory(userId, limit);
-
-                res.status(200).json({
-                    success: true,
-                    count: history.length,
-                    history: history
-                });
-
-            } catch (error) {
-                console.error('Erreur dans getHistory controller:', error);
-
-                res.status(500).json({
-                    success: false,
-                    error: 'Erreur serveur'
-                });
-            }
-        },
-
-        // =========================================================================================
-        //                                         CRUD
-        //==========================================================================================
-
-
-        // =====================================================
-        // POST /api/alerts/rules - CRÉER UNE RÈGLE
-        // =====================================================
-        /**
-         * Crée une nouvelle règle d'alerte
-         * 
-         * Body attendu :
-         * {
-         *   "userId": "uuid",
-         *   "pairId": 1,
-         *   "channel": "EMAIL",
-         *   "minConfidence": 0.8,
-         *   "severity": "WARNING",
-         *   "cooldownMs": 60000
-         * }
-         */
-        async createRule(req, res) {
-            try {
-                const ruleData = req.body;
-
-                // Validation des champs obligatoires
-                if (!ruleData.userId || !ruleData.channel) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'Champs manquants (userId, channel requis)'
-                    });
-                }
-
-                // Appeler le repo (qui appelle l'adapter)
-                const newRule = await alertsRepo.createRule(ruleData);
-
-                res.status(201).json({
-                    success: true,
-                    rule: newRule,
-                    message: 'Règle créée avec succès'
-                });
-
-            } catch (error) {
-                console.error('❌ Erreur createRule:', error);
-                res.status(500).json({
-                    success: false,
-                    error: error.message
-                });
-            }
-        },
-
-        // =====================================================
-        // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE
-        // =====================================================
-        /**
-         * Met à jour une règle existante
-         * 
-         * Body possible :
-         * {
-         *   "minConfidence": 0.9,
-         *   "enabled": true,
-         *   "channel": "CONSOLE",
-         *   "severity": "CRITICAL"
-         * }
-         */
-        async updateRule(req, res) {
-            try {
-                const { id } = req.params;
-                const updates = req.body;
-
-                // Vérifier qu'il y a quelque chose à mettre à jour
-                if (Object.keys(updates).length === 0) {
-                    return res.status(400).json({
-                        success: false,
-                        error: 'Aucune donnée à mettre à jour'
-                    });
-                }
-
-                // Appeler le repo (qui appelle l'adapter)
-                const updated = await alertsRepo.updateRule(id, updates);
-
-                if (!updated) {
-                    return res.status(404).json({
-                        success: false,
-                        error: 'Règle non trouvée'
-                    });
-                }
-
-                res.json({
-                    success: true,
-                    message: 'Règle mise à jour'
-                });
-
-            } catch (error) {
-                console.error('❌ Erreur updateRule:', error);
-                res.status(500).json({
-                    success: false,
-                    error: error.message
-                });
-            }
-        },
-
-        // =====================================================
-        // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE
-        // =====================================================
-        async deleteRule(req, res) {
-            try {
-                const { id } = req.params;
-
-                // Appeler le repo (qui appelle l'adapter)
-                const deleted = await alertsRepo.deleteRule(id);
-
-                if (!deleted) {
-                    return res.status(404).json({
-                        success: false,
-                        error: 'Règle non trouvée'
-                    });
-                }
-
-                res.json({
-                    success: true,
-                    message: 'Règle supprimée'
-                });
-
-            } catch (error) {
-                console.error('❌ Erreur deleteRule:', error);
-                res.status(500).json({
-                    success: false,
-                    error: error.message
-                });
-            }
-        },
-
-        // =====================================================
-        // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE
-        // =====================================================
-        async getRuleById(req, res) {
-            try {
-                const { id } = req.params;
-
-                const rule = await alertsRepo.findRuleById(id);
-
-                if (!rule) {
-                    return res.status(404).json({
-                        success: false,
-                        error: 'Règle non trouvée'
-                    });
-                }
-
-                res.json({
-                    success: true,
-                    rule
-                });
-
-            } catch (error) {
-                console.error('❌ Erreur getRuleById:', error);
-                res.status(500).json({
-                    success: false,
-                    error: error.message
-                });
-            }
-        }
-    };
-}
-
+// =========================================================\r
+// ALERTS CONTROLLER - VERSION RÉUTILISABLE\r
+// =========================================================\r
+// RÔLE : Gestion des requêtes et réponses HTTP\r
+// =========================================================\r
+\r
+\r
+/**\r
+ * Crée un controller d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsService - Service d'alertes (créé par createAlertsService)\r
+ * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes)\r
+ *\r
+ * @returns {Object} Controller avec toutes les méthodes\r
+ */\r
+export function createAlertsController(alertsService, alertsRepo) {\r
+\r
+    console.log("Controller d'alertes initialisé");\r
+\r
+    return {\r
+\r
+        // =====================================================\r
+        // POST /api/alerts/process-signal\r
+        // =====================================================\r
+        /**\r
+         * Endpoint pour traiter un signal crypto\r
+         *\r
+         * Appelé par le module Strategy (Sacha) quand il\r
+         * génère un signal BUY/SELL/HOLD\r
+         *\r
+         * Body attendu :\r
+         * {\r
+         *   "userId": "uuid",\r
+         *   "pairId": 1,\r
+         *   "pair": "BTC/EUR",\r
+         *   "action": "BUY",\r
+         *   "confidence": 0.87,\r
+         *   "criticality": "WARNING",\r
+         *   "reason": "Indicators show...",\r
+         *   "priceAtSignal": 45000.50\r
+         * }\r
+         */\r
+        async processSignal(req, res) {\r
+            try {\r
+                // ==================================================================================\r
+                // 1. RÉCUPÉRER LES DONNÉES DU BODY\r
+                // ==================================================================================\r
+                const signal = req.body;\r
+\r
+                // ==================================================================================\r
+                // 2. VALIDATION DES DONNÉES\r
+                // ==================================================================================\r
+                // Vérifier que tous les champs obligatoires sont présents\r
+                const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality'];\r
+                /**\r
+                 *  on remplit le tableau des champs manquant ou faux du contenu de signal\r
+                 */\r
+                const missingFields = requiredFields.filter(field => !signal[field]);\r
+                // dans ce cas on retourne une erreur et le noms des champs en cause\r
+                if (missingFields.length > 0) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'Champs manquants',\r
+                        missingFields: missingFields\r
+                    });\r
+                }\r
+\r
+                // Valider le format de action\r
+                const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS'];\r
+                if (!validActions.includes(signal.action)) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: `Action invalide. Doit être: ${validActions.join(', ')}`\r
+                    });\r
+                }\r
+\r
+                // Valider le format de criticality\r
+                const validCriticalities = ['CRITICAL', 'WARNING', 'INFO'];\r
+                if (!validCriticalities.includes(signal.criticality)) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}`\r
+                    });\r
+                }\r
+\r
+                // Valider la confidence (doit être entre 0 et 1)\r
+                if (signal.confidence < 0 || signal.confidence > 1) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'Confidence doit être entre 0 et 1'\r
+                    });\r
+                }\r
+\r
+                // =================================================================================\r
+                // 3. APPELER LE SERVICE (injecté)\r
+                // =================================================================================\r
+                // Le service contient toute la logique\r
+                await alertsService.processSignal(signal);\r
+\r
+                // =================================================================================\r
+                // 4. RÉPONDRE AU CLIENT\r
+                // =================================================================================\r
+                res.status(200).json({\r
+                    success: true,\r
+                    message: 'Signal traité avec succès',\r
+                    signal: {\r
+                        action: signal.action,\r
+                        pair: signal.pair,\r
+                        confidence: signal.confidence\r
+                    }\r
+                });\r
+\r
+            } catch (error) {\r
+                // =================================================================================\r
+                // GESTION DES ERREURS\r
+                // =================================================================================\r
+                console.error(' Erreur dans processSignal controller:', error);\r
+\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: 'Erreur serveur lors du traitement du signal',\r
+                    details: process.env.NODE_ENV === 'development' ? error.message : undefined\r
+                });\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // GET /api/alerts/rules/:userId\r
+        // =====================================================\r
+        /**\r
+         * Récupère toutes les règles d'alerte d'un utilisateur\r
+         *\r
+         * Utile pour afficher la configuration dans l'interface\r
+         */\r
+        async getRules(req, res) {\r
+            try {\r
+                const { userId } = req.params;\r
+\r
+                // Validation\r
+                if (!userId) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'userId manquant'\r
+                    });\r
+                }\r
+\r
+                // Récupérer les règles (via repo injecté)\r
+                const rules = await alertsRepo.findActiveRulesForSignal(userId, null);\r
+\r
+                res.status(200).json({\r
+                    success: true,\r
+                    count: rules.length,\r
+                    rules: rules\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('Erreur dans getRules controller:', error);\r
+\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: 'Erreur serveur'\r
+                });\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // GET /api/alerts/history/:userId\r
+        // =====================================================\r
+        /**\r
+         * Récupère l'historique des alertes d'un utilisateur\r
+         */\r
+        async getHistory(req, res) {\r
+            try {\r
+                const { userId } = req.params;\r
+                const limit = parseInt(req.query.limit) || 50;\r
+\r
+                if (!userId) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'userId manquant'\r
+                    });\r
+                }\r
+\r
+                // Récupérer l'historique (via repo injecté)\r
+                const history = await alertsRepo.getAlertHistory(userId, limit);\r
+\r
+                res.status(200).json({\r
+                    success: true,\r
+                    count: history.length,\r
+                    history: history\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('Erreur dans getHistory controller:', error);\r
+\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: 'Erreur serveur'\r
+                });\r
+            }\r
+        },\r
+\r
+        // =========================================================================================\r
+        //                                         CRUD\r
+        //==========================================================================================\r
+\r
+\r
+        // =====================================================\r
+        // POST /api/alerts/rules - CRÉER UNE RÈGLE\r
+        // =====================================================\r
+        /**\r
+         * Crée une nouvelle règle d'alerte\r
+         * \r
+         * Body attendu :\r
+         * {\r
+         *   "userId": "uuid",\r
+         *   "pairId": 1,\r
+         *   "channel": "EMAIL",\r
+         *   "minConfidence": 0.8,\r
+         *   "severity": "WARNING",\r
+         *   "cooldownMs": 60000\r
+         * }\r
+         */\r
+        async createRule(req, res) {\r
+            try {\r
+                const ruleData = req.body;\r
+\r
+                // Validation des champs obligatoires\r
+                if (!ruleData.userId || !ruleData.channel) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'Champs manquants (userId, channel requis)'\r
+                    });\r
+                }\r
+\r
+                // Appeler le repo (qui appelle l'adapter)\r
+                const newRule = await alertsRepo.createRule(ruleData);\r
+\r
+                res.status(201).json({\r
+                    success: true,\r
+                    rule: newRule,\r
+                    message: 'Règle créée avec succès'\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('❌ Erreur createRule:', error);\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: error.message\r
+                });\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE\r
+        // =====================================================\r
+        /**\r
+         * Met à jour une règle existante\r
+         * \r
+         * Body possible :\r
+         * {\r
+         *   "minConfidence": 0.9,\r
+         *   "enabled": true,\r
+         *   "channel": "CONSOLE",\r
+         *   "severity": "CRITICAL"\r
+         * }\r
+         */\r
+        async updateRule(req, res) {\r
+            try {\r
+                const { id } = req.params;\r
+                const updates = req.body;\r
+\r
+                // Vérifier qu'il y a quelque chose à mettre à jour\r
+                if (Object.keys(updates).length === 0) {\r
+                    return res.status(400).json({\r
+                        success: false,\r
+                        error: 'Aucune donnée à mettre à jour'\r
+                    });\r
+                }\r
+\r
+                // Appeler le repo (qui appelle l'adapter)\r
+                const updated = await alertsRepo.updateRule(id, updates);\r
+\r
+                if (!updated) {\r
+                    return res.status(404).json({\r
+                        success: false,\r
+                        error: 'Règle non trouvée'\r
+                    });\r
+                }\r
+\r
+                res.json({\r
+                    success: true,\r
+                    message: 'Règle mise à jour'\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('❌ Erreur updateRule:', error);\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: error.message\r
+                });\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE\r
+        // =====================================================\r
+        async deleteRule(req, res) {\r
+            try {\r
+                const { id } = req.params;\r
+\r
+                // Appeler le repo (qui appelle l'adapter)\r
+                const deleted = await alertsRepo.deleteRule(id);\r
+\r
+                if (!deleted) {\r
+                    return res.status(404).json({\r
+                        success: false,\r
+                        error: 'Règle non trouvée'\r
+                    });\r
+                }\r
+\r
+                res.json({\r
+                    success: true,\r
+                    message: 'Règle supprimée'\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('❌ Erreur deleteRule:', error);\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: error.message\r
+                });\r
+            }\r
+        },\r
+\r
+        // =====================================================\r
+        // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE\r
+        // =====================================================\r
+        async getRuleById(req, res) {\r
+            try {\r
+                const { id } = req.params;\r
+\r
+                const rule = await alertsRepo.findRuleById(id);\r
+\r
+                if (!rule) {\r
+                    return res.status(404).json({\r
+                        success: false,\r
+                        error: 'Règle non trouvée'\r
+                    });\r
+                }\r
+\r
+                res.json({\r
+                    success: true,\r
+                    rule\r
+                });\r
+\r
+            } catch (error) {\r
+                console.error('❌ Erreur getRuleById:', error);\r
+                res.status(500).json({\r
+                    success: false,\r
+                    error: error.message\r
+                });\r
+            }\r
+        }\r
+    };\r
+}\r
+\r
index 8c0e288ffec394878f04269ce6aa3017189c6602..63cac345a9b616609ab9f85eaa9b62f3c756dc69 100644 (file)
-// =========================================================
-// ALERTS REPOSITORY - VERSION AVEC ADAPTER
-// =========================================================
-// RÔLE : Interface entre le Service et l'Adapter
-// =========================================================
-// Le repo ne fait PLUS de SQL directement.
-// Il délègue à l'adapter injecté.
-// =========================================================
-
-// =========================================================
-// FACTORY FUNCTION : Créer un repository
-// =========================================================
-/**
- * Crée un repository d'alertes réutilisable
- *
- * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)
- * @returns {Object} Repository avec toutes les méthodes
- *
- * @example
- * // Avec MySQL
- * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
- * import { createAlertsRepo } from './alerts.repo.js';
- *
- * const adapter = createMySQLAdapter(db);
- * const repo = createAlertsRepo(adapter);
- *
- * @example
- * // Avec MongoDB (adapter à créer)
- * import { createMongoAdapter } from './adapters/mongo.adapter.js';
- *
- * const adapter = createMongoAdapter(mongoClient);
- * const repo = createAlertsRepo(adapter);
- */
-export function createAlertsRepo(adapter) {
-
-    console.log(`Repository d'alertes initialisé`);
-
-// =========================================================
-// RETOURNER L'OBJET REPOSITORY
-// =========================================================
-    return {
-
-        // =====================================================
-        // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
-        // =====================================================
-        /**
-         * Récupère les règles d'alerte pour un signal donné
-         * @param {string} userId - ID de l'utilisateur
-         * @param {number} pairId - ID de la paire crypto
-         * @returns {Promise<Array>} Liste des règles actives
-         */
-        async findActiveRulesForSignal(userId, pairId) {
-            return adapter.findActiveRulesForSignal(userId, pairId);
-        },
-
-        // =====================================================
-        // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
-        // =====================================================
-        /**
-         * Enregistre qu'une alerte a été envoyée
-         * @param {Object} eventData - Données de l'événement
-         */
-        async saveAlertEvent(eventData) {
-            return adapter.saveAlertEvent(eventData);
-        },
-
-        // =====================================================
-        // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
-        // =====================================================
-        /**
-         * Met à jour le timestamp pour le cooldown (anti-spam)
-         * @param {string} ruleId - ID de la règle
-         * @param {number} timestamp - Timestamp en millisecondes
-         */
-        async updateLastNotified(ruleId, timestamp) {
-            return adapter.updateLastNotified(ruleId, timestamp);
-        },
-
-        // =====================================================
-        // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
-        // =====================================================
-        /**
-         * Récupère les dernières alertes d'un utilisateur
-         * @param {string} userId - ID de l'utilisateur
-         * @param {number} limit - Nombre max de résultats
-         * @returns {Promise<Array>} Historique des alertes
-         */
-        async getAlertHistory(userId, limit = 50) {
-            return adapter.getAlertHistory(userId, limit);
-        },
-
-        // =====================================================
-        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
-        // =====================================================
-        /**
-         * Crée une nouvelle règle d'alerte
-         * @param {Object} ruleData - Données de la règle
-         * @returns {Promise<Object>} Règle créée
-         */
-        async createRule(ruleData) {
-            return adapter.createRule(ruleData);
-        },
-
-        // =====================================================
-        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
-        // =====================================================
-        /**
-         * Met à jour une règle existante
-         * @param {string} ruleId - ID de la règle
-         * @param {Object} updates - Champs à mettre à jour
-         * @returns {Promise<boolean>} true si modifié
-         */
-        async updateRule(ruleId, updates) {
-            return adapter.updateRule(ruleId, updates);
-        },
-
-        // =====================================================
-        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
-        // =====================================================
-        /**
-         * Supprime une règle
-         * @param {string} ruleId - ID de la règle
-         * @returns {Promise<boolean>} true si supprimé
-         */
-        async deleteRule(ruleId) {
-            return adapter.deleteRule(ruleId);
-        },
-
-        // =====================================================
-        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
-        // =====================================================
-        /**
-         * Récupère une règle par son ID
-         * @param {string} ruleId - ID de la règle
-         * @returns {Promise<Object|null>} La règle ou null
-         */
-        async findRuleById(ruleId) {
-            return adapter.findRuleById(ruleId);
-        },
-    };
-}
+// =========================================================\r
+// ALERTS REPOSITORY - VERSION AVEC ADAPTER\r
+// =========================================================\r
+// RÔLE : Interface entre le Service et l'Adapter\r
+// =========================================================\r
+// Le repo ne fait PLUS de SQL directement.\r
+// Il délègue à l'adapter injecté.\r
+// =========================================================\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un repository\r
+// =========================================================\r
+/**\r
+ * Crée un repository d'alertes réutilisable\r
+ *\r
+ * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)\r
+ * @returns {Object} Repository avec toutes les méthodes\r
+ *\r
+ * @example\r
+ * // Avec MySQL\r
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+ * import { createAlertsRepo } from './alerts.repo.js';\r
+ *\r
+ * const adapter = createMySQLAdapter(db);\r
+ * const repo = createAlertsRepo(adapter);\r
+ *\r
+ * @example\r
+ * // Avec MongoDB (adapter à créer)\r
+ * import { createMongoAdapter } from './adapters/mongo.adapter.js';\r
+ *\r
+ * const adapter = createMongoAdapter(mongoClient);\r
+ * const repo = createAlertsRepo(adapter);\r
+ */\r
+export function createAlertsRepo(adapter) {\r
+\r
+    console.log(`Repository d'alertes initialisé`);\r
+\r
+// =========================================================\r
+// RETOURNER L'OBJET REPOSITORY\r
+// =========================================================\r
+    return {\r
+\r
+        // =====================================================\r
+        // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES\r
+        // =====================================================\r
+        /**\r
+         * Récupère les règles d'alerte pour un signal donné\r
+         * @param {string} userId - ID de l'utilisateur\r
+         * @param {number} pairId - ID de la paire crypto\r
+         * @returns {Promise<Array>} Liste des règles actives\r
+         */\r
+        async findActiveRulesForSignal(userId, pairId) {\r
+            return adapter.findActiveRulesForSignal(userId, pairId);\r
+        },\r
+\r
+        // =====================================================\r
+        // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE\r
+        // =====================================================\r
+        /**\r
+         * Enregistre qu'une alerte a été envoyée\r
+         * @param {Object} eventData - Données de l'événement\r
+         */\r
+        async saveAlertEvent(eventData) {\r
+            return adapter.saveAlertEvent(eventData);\r
+        },\r
+\r
+        // =====================================================\r
+        // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION\r
+        // =====================================================\r
+        /**\r
+         * Met à jour le timestamp pour le cooldown (anti-spam)\r
+         * @param {string} ruleId - ID de la règle\r
+         * @param {number} timestamp - Timestamp en millisecondes\r
+         */\r
+        async updateLastNotified(ruleId, timestamp) {\r
+            return adapter.updateLastNotified(ruleId, timestamp);\r
+        },\r
+\r
+        // =====================================================\r
+        // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES\r
+        // =====================================================\r
+        /**\r
+         * Récupère les dernières alertes d'un utilisateur\r
+         * @param {string} userId - ID de l'utilisateur\r
+         * @param {number} limit - Nombre max de résultats\r
+         * @returns {Promise<Array>} Historique des alertes\r
+         */\r
+        async getAlertHistory(userId, limit = 50) {\r
+            return adapter.getAlertHistory(userId, limit);\r
+        },\r
+\r
+        // =====================================================\r
+        // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)\r
+        // =====================================================\r
+        /**\r
+         * Crée une nouvelle règle d'alerte\r
+         * @param {Object} ruleData - Données de la règle\r
+         * @returns {Promise<Object>} Règle créée\r
+         */\r
+        async createRule(ruleData) {\r
+            return adapter.createRule(ruleData);\r
+        },\r
+\r
+        // =====================================================\r
+        // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)\r
+        // =====================================================\r
+        /**\r
+         * Met à jour une règle existante\r
+         * @param {string} ruleId - ID de la règle\r
+         * @param {Object} updates - Champs à mettre à jour\r
+         * @returns {Promise<boolean>} true si modifié\r
+         */\r
+        async updateRule(ruleId, updates) {\r
+            return adapter.updateRule(ruleId, updates);\r
+        },\r
+\r
+        // =====================================================\r
+        // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)\r
+        // =====================================================\r
+        /**\r
+         * Supprime une règle\r
+         * @param {string} ruleId - ID de la règle\r
+         * @returns {Promise<boolean>} true si supprimé\r
+         */\r
+        async deleteRule(ruleId) {\r
+            return adapter.deleteRule(ruleId);\r
+        },\r
+\r
+        // =====================================================\r
+        // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)\r
+        // =====================================================\r
+        /**\r
+         * Récupère une règle par son ID\r
+         * @param {string} ruleId - ID de la règle\r
+         * @returns {Promise<Object|null>} La règle ou null\r
+         */\r
+        async findRuleById(ruleId) {\r
+            return adapter.findRuleById(ruleId);\r
+        },\r
+    };\r
+}\r
index 8280d868765c2ea2302c0bf22c41767bb2c7c3a9..bc7ac7865f1967343e3592d6e2f670acdfab2cba 100644 (file)
@@ -1,80 +1,80 @@
-// =========================================================
-// ALERTS ROUTER - VERSION RÉUTILISABLE
-// =========================================================
-// RÔLE : Définir les routes (endpoints) de l'API
-// =========================================================
-
-
-import express from 'express';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un router
-// =========================================================
-/**
- * Crée un router d'alertes réutilisable
- *
- * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController)
- *
- * @returns {express.Router} Router Express configuré
- */
-export function createAlertsRouter(alertsController) {
-
-    const router = express.Router();
-
-    console.log("Router d'alertes initialisé");
-
-// =========================================================
-    // DÉFINITION DES ROUTES
-// =========================================================
-    // POST /api/alerts/process-signal
-// =========================================================
-    /**
-     * Traiter un signal crypto et envoyer les alertes
-     *
-     * Exemple d'appel :
-     * POST http://localhost:3000/api/alerts/process-signal
-     * Body : {
-     *   "userId": "user-123",
-     *   "pairId": 1,
-     *   "pair": "BTC/EUR",
-     *   "action": "BUY",
-     *   "confidence": 0.85,
-     *   "criticality": "WARNING",
-     *   "reason": "RSI oversold + MACD crossover"
-     * }
-     */
-    router.post('/process-signal', alertsController.processSignal);
-
-// =============================================================================
-    // GET /api/alerts/rules/:userId
-// =============================================================================
-    /**
-     * Récupérer les règles d'alerte d'un utilisateur
-     *
-     * Exemple d'appel :
-     * GET http://localhost:3000/api/alerts/rules/user-123
-     */
-    router.get('/rules/:userId', alertsController.getRules);
-
-// =============================================================================
-    // GET /api/alerts/history/:userId
-// =============================================================================
-    /**
-    * Récupérer l'historique des alertes d'un utilisateur
-     *
-     * Exemple d'appel :
-     * GET http://localhost:3000/api/alerts/history/user-123?limit=20
-     */
-    router.get('/history/:userId', alertsController.getHistory);
-
-// =============================================================================
-//                                  CRUD
-// =============================================================================
-    router.post('/rules', alertsController.createRule);
-    router.get('/rules/detail/:id', alertsController.getRuleById);
-    router.put('/rules/:id', alertsController.updateRule);
-    router.delete('/rules/:id', alertsController.deleteRule);
-
-    return router;
-}
-
+// =========================================================\r
+// ALERTS ROUTER - VERSION RÉUTILISABLE\r
+// =========================================================\r
+// RÔLE : Définir les routes (endpoints) de l'API\r
+// =========================================================\r
+\r
+\r
+import express from 'express';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un router\r
+// =========================================================\r
+/**\r
+ * Crée un router d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController)\r
+ *\r
+ * @returns {express.Router} Router Express configuré\r
+ */\r
+export function createAlertsRouter(alertsController) {\r
+\r
+    const router = express.Router();\r
+\r
+    console.log("Router d'alertes initialisé");\r
+\r
+// =========================================================\r
+    // DÉFINITION DES ROUTES\r
+// =========================================================\r
+    // POST /api/alerts/process-signal\r
+// =========================================================\r
+    /**\r
+     * Traiter un signal crypto et envoyer les alertes\r
+     *\r
+     * Exemple d'appel :\r
+     * POST http://localhost:3000/api/alerts/process-signal\r
+     * Body : {\r
+     *   "userId": "user-123",\r
+     *   "pairId": 1,\r
+     *   "pair": "BTC/EUR",\r
+     *   "action": "BUY",\r
+     *   "confidence": 0.85,\r
+     *   "criticality": "WARNING",\r
+     *   "reason": "RSI oversold + MACD crossover"\r
+     * }\r
+     */\r
+    router.post('/process-signal', alertsController.processSignal);\r
+\r
+// =============================================================================\r
+    // GET /api/alerts/rules/:userId\r
+// =============================================================================\r
+    /**\r
+     * Récupérer les règles d'alerte d'un utilisateur\r
+     *\r
+     * Exemple d'appel :\r
+     * GET http://localhost:3000/api/alerts/rules/user-123\r
+     */\r
+    router.get('/rules/:userId', alertsController.getRules);\r
+\r
+// =============================================================================\r
+    // GET /api/alerts/history/:userId\r
+// =============================================================================\r
+    /**\r
+    * Récupérer l'historique des alertes d'un utilisateur\r
+     *\r
+     * Exemple d'appel :\r
+     * GET http://localhost:3000/api/alerts/history/user-123?limit=20\r
+     */\r
+    router.get('/history/:userId', alertsController.getHistory);\r
+\r
+// =============================================================================\r
+//                                  CRUD\r
+// =============================================================================\r
+    router.post('/rules', alertsController.createRule);\r
+    router.get('/rules/detail/:id', alertsController.getRuleById);\r
+    router.put('/rules/:id', alertsController.updateRule);\r
+    router.delete('/rules/:id', alertsController.deleteRule);\r
+\r
+    return router;\r
+}\r
+\r
index 4280614f35472362f3a4d3debeb45f1cf61d909c..8df13fb64a1328721b1786aac43430ad5c2c5f3b 100644 (file)
-// =========================================================
-// ALERTS SERVICE - VERSION RÉUTILISABLE
-// =========================================================
-// 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';
-
-// Import des canaux
-import { sendAlertEmail } from './channels/mailer.js';
-import { sendConsoleAlert } from './channels/console.js';
-import { sendTelegramAlert } from './channels/telegram.js';
-import { sendDiscordAlert } from './channels/discord.js';
-import { sendWebAlert } from './channels/web.js';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un service
-// =========================================================
-/**
- * Crée un service d'alertes réutilisable
- *
- * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo)
- * @param {Object} options - Configuration optionnelle
- * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min)
- *
- * @returns {Object} Service avec toutes les méthodes
- *
- * @example
- * // Dans Wall-e-tte
- * import db from './config/db.js';
- * import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
- * import { createAlertsService } from './modules/alerts/alerts.service.js';
- *
- * const repo = createAlertsRepo(db);
- * const service = createAlertsService(repo);
- *
- * await service.processSignal(signal);
- *
- * @example
- * // Dans un autre projet
- * const repo = createAlertsRepo(strangersDb, { ... });
- * const service = createAlertsService(repo);
- * await service.processSignal(signal);  // Marche pareil !
- */
-export function createAlertsService(alertsRepo, options = {}) {
-
-    console.log('🔧 Service d\'alertes initialisé');
-
-    // ─────────────────────────────────────────────────────
-    // RETOURNER L'OBJET SERVICE
-    // ─────────────────────────────────────────────────────
-    return {
-
-    // =====================================================
-    // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE
-    // =====================================================
-    /**
-     * 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
-     * - Vérification du cooldown (anti-spam)
-     *
-     * @param {Object} rule - Règle d'alerte
-     * @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}...`);
-
-        // ─────────────────────────────────────────────────
-        // 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)}%`);
-            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
-        //           → Trop tôt, ne pas envoyer
-
-        const now = Date.now();
-
-        if (rule.last_notified_at_ms) {
-            // Calculer le temps écoulé depuis la dernière alerte
-            const timeSinceLastAlert = now - rule.last_notified_at_ms;
-
-            // Si pas assez de temps écoulé, ne pas envoyer
-            if (timeSinceLastAlert < rule.cooldown_ms) {
-                const remainingMs = rule.cooldown_ms - timeSinceLastAlert;
-                const remainingSec = Math.ceil(remainingMs / 1000);
-
-                console.log(`   ⏳ Cooldown actif`);
-                console.log(`      Attendre encore ${remainingSec}s`);
-                return false;
-            }
-        }
-
-        // ─────────────────────────────────────────────────
-        // TOUS LES CRITÈRES SONT OK !
-        // ─────────────────────────────────────────────────
-        console.log(`   Tous les critères sont remplis`);
-        return true;
-    },
-
-    // =====================================================
-    // 2. ENVOYER UNE ALERTE VIA LE BON CANAL
-    // =====================================================
-    /**
-     * Envoie une alerte via le canal spécifié dans la règle
-     *
-     * Cette fonction :
-     * - Choisit la bonne fonction d'envoi (email, telegram, etc.)
-     * - Prépare les paramètres spécifiques au canal
-     * - Gère les erreurs d'envoi
-     *
-     * @param {Object} rule - Règle d'alerte avec le canal
-     * @param {Object} signal - Signal à envoyer
-     * @returns {Promise<string>} 'SENT' ou 'FAILED'
-     */
-    async sendViaChannel(rule, signal) {
-        console.log(`Envoi via ${rule.channel}...`);
-
-        let status = 'SENT';
-
-        try {
-            // ─────────────────────────────────────────────
-            // SWITCH selon le canal configuré
-            // ─────────────────────────────────────────────
-            switch (rule.channel) {
-
-                // ── 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 ─────────────────────────────
-                case 'TELEGRAM':
-                    // Le chatId est stocké dans rule.params (JSON)
-                    // Exemple de params : {"chatId": "123456789"}
-                    const params = JSON.parse(rule.params);
-                    const chatId = params.chatId;
-
-                    if (!chatId) {
-                        console.error(' Pas de chatId dans params');
-                        status = 'FAILED';
-                        break;
-                    }
-
-                    await sendTelegramAlert(chatId, signal);
-                    break;
-
-                // ── DISCORD ──────────────────────────────
-                case 'DISCORD':
-                    const discordParams = JSON.parse(rule.params);
-                    const webhookUrl = discordParams.webhookUrl;
-
-                    if (!webhookUrl) {
-                        console.error(' Pas de webhookUrl dans params');
-                        status = 'FAILED';
-                        break;
-                    }
-
-                    await sendDiscordAlert(webhookUrl, signal);
-                    break;
-
-                // ── CONSOLE ──────────────────────────────
-                case 'CONSOLE':
-                    // Pas de paramètres nécessaires
-                    await sendConsoleAlert(signal);
-                    break;
-
-                // ── WEB ──────────────────────────────────
-                case 'WEB':
-                    // Envoyer à l'interface web de l'utilisateur
-                    await sendWebAlert(rule.user_id, signal);
-                    break;
-
-                // ── CANAL INCONNU ────────────────────────
-                default:
-                    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}`);
-            status = 'FAILED';
-        }
-
-        return status;
-    },
-
-        // =====================================================
-        // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES
-        // =====================================================
-        /**
-         * Fonction principale qui traite un signal crypto
-         *
-         * 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
-         *    b. Envoyer via le bon canal
-         *    c. Enregistrer dans l'historique
-         *    d. Mettre à jour le timestamp
-         *
-         * @param {Object} signal - Signal crypto reçu
-         * @param {string} signal.userId - ID utilisateur
-         * @param {number} signal.pairId - ID de la paire
-         * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
-         * @param {number} signal.confidence - 0-1
-         * @param {string} signal.criticality - CRITICAL, WARNING, INFO
-         * @param {string} signal.reason - Explication
-         */
-        async processSignal(signal) {
-            console.log('\n' + '═'.repeat(80));
-            console.log(` TRAITEMENT DU SIGNAL`);
-            console.log(`   Action : ${signal.action}`);
-            console.log(`   Paire  : ${signal.pair}`);
-            console.log(`   User   : ${signal.userId}`);
-            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,
-                    signal.pairId
-                );
-
-                if (rules.length === 0) {
-                    console.log(' Aucune règle active trouvée pour ce signal');
-                    console.log('   → Vérifie que des règles sont configurées en DB\n');
-                    return;
-                }
-
-                console.log(` ${rules.length} règle(s) trouvée(s)\n`);
-
-                // ─────────────────────────────────────────────
-                // ÉTAPE 2 : Traiter chaque règle
-                // ─────────────────────────────────────────────
-                let sentCount = 0;
-
-                for (const rule of rules) {
-                    console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);
-
-                    // Vérifier si on doit envoyer
-                    if (!this.shouldSendAlert(rule, signal)) {
-                        console.log(`└─  Alerte NON envoyée\n`);
-                        continue;
-                    }
-
-                    // Envoyer via le canal
-                    const status = await this.sendViaChannel(rule, signal);
-
-                    // Préparer les données de l'événement
-                    const eventData = {
-                        alert_event_id: uuidv4(),
-                        rule_id: rule.rule_id,
-                        timestamp_ms: Date.now(),
-                        severity: signal.criticality,
-                        channel: rule.channel,
-                        send_status: status,
-                        title: `${signal.action} ${signal.pair}`,
-                        message: signal.reason,
-                        signal_action: signal.action,
-                        confidence: signal.confidence,
-                        correlation_id: signal.correlationId || null
-                    };
-
-                    // Sauvegarder dans alert_events (via repo injecté)
-                    await alertsRepo.saveAlertEvent(eventData);
-
-                    // Mettre à jour last_notified_at si envoi réussi (via repo injecté)
-                    if (status === 'SENT') {
-                        await alertsRepo.updateLastNotified(rule.rule_id, Date.now());
-                        sentCount++;
-                        console.log(`└─  Alerte envoyée avec succès\n`);
-                    } else {
-                        console.log(`└─  Échec de l'envoi\n`);
-                    }
-                }
-
-                // ─────────────────────────────────────────────
-                // RÉSUMÉ FINAL
-                // ─────────────────────────────────────────────
-                console.log('═'.repeat(80));
-                console.log(` RÉSUMÉ`);
-                console.log(`   Règles vérifiées : ${rules.length}`);
-                console.log(`   Alertes envoyées : ${sentCount}`);
-                console.log('═'.repeat(80) + '\n');
-
-            } catch (error) {
-                console.error(' ERREUR CRITIQUE dans processSignal:');
-                console.error(error);
-                throw error; // Remonter l'erreur pour que le controller puisse la gérer
-            }
-        }
-    };
-}
+// =========================================================\r
+// ALERTS SERVICE - VERSION RÉUTILISABLE\r
+// =========================================================\r
+// RÔLE : Logique métier du système d'alertes\r
+// =========================================================\r
+// PRINCIPE : INJECTION DE DÉPENDANCES\r
+// Au lieu d'importer le Repository directement, on le reçoit en paramètre\r
+// =========================================================\r
+\r
+import { v4 as uuidv4 } from 'uuid';\r
+\r
+// Import des canaux\r
+import { sendAlertEmail } from './channels/mailer.js';\r
+import { sendConsoleAlert } from './channels/console.js';\r
+import { sendTelegramAlert } from './channels/telegram.js';\r
+import { sendDiscordAlert } from './channels/discord.js';\r
+import { sendWebAlert } from './channels/web.js';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un service\r
+// =========================================================\r
+/**\r
+ * Crée un service d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo)\r
+ * @param {Object} options - Configuration optionnelle\r
+ * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min)\r
+ *\r
+ * @returns {Object} Service avec toutes les méthodes\r
+ *\r
+ * @example\r
+ * // Dans Wall-e-tte\r
+ * import db from './config/db.js';\r
+ * import { createAlertsRepo } from './modules/alerts/alerts.repo.js';\r
+ * import { createAlertsService } from './modules/alerts/alerts.service.js';\r
+ *\r
+ * const repo = createAlertsRepo(db);\r
+ * const service = createAlertsService(repo);\r
+ *\r
+ * await service.processSignal(signal);\r
+ *\r
+ * @example\r
+ * // Dans un autre projet\r
+ * const repo = createAlertsRepo(strangersDb, { ... });\r
+ * const service = createAlertsService(repo);\r
+ * await service.processSignal(signal);  // Marche pareil !\r
+ */\r
+export function createAlertsService(alertsRepo, options = {}) {\r
+\r
+    console.log('🔧 Service d\'alertes initialisé');\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // RETOURNER L'OBJET SERVICE\r
+    // ─────────────────────────────────────────────────────\r
+    return {\r
+\r
+    // =====================================================\r
+    // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE\r
+    // =====================================================\r
+    /**\r
+     * Détermine si une alerte doit être envoyée selon la règle\r
+     *\r
+     * Cette fonction applique les RÈGLES MÉTIER :\r
+     * - Vérification de la confidence minimum\r
+     * - Vérification du cooldown (anti-spam)\r
+     *\r
+     * @param {Object} rule - Règle d'alerte\r
+     * @param {Object} signal - Signal reçu\r
+     * @returns {boolean} true si l'alerte doit être envoyée\r
+     */\r
+    shouldSendAlert(rule, signal) {\r
+        console.log(`Vérification règle ${rule.rule_id}...`);\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // CRITÈRE 1 : Vérifier la confidence minimum\r
+        // ─────────────────────────────────────────────────\r
+        // Si le signal a une confidence trop basse, on n'envoie pas\r
+        // Exemple : rule.min_confidence = 0.80 (80%)\r
+        //           signal.confidence = 0.75 (75%)\r
+        //           → Ne pas envoyer (75% < 80%)\r
+\r
+        if (signal.confidence < rule.min_confidence) {\r
+            console.log(`Confidence trop basse`);\r
+            console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`);\r
+            console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`);\r
+            return false;\r
+        }\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // CRITÈRE 2 : Vérifier le cooldown (anti-spam)\r
+        // ─────────────────────────────────────────────────\r
+        // Le cooldown évite d'envoyer 100 emails en 1 minute\r
+        // Exemple : cooldown_ms = 60000 (1 minute)\r
+        //           Dernière alerte envoyée il y a 30 secondes\r
+        //           → Trop tôt, ne pas envoyer\r
+\r
+        const now = Date.now();\r
+\r
+        if (rule.last_notified_at_ms) {\r
+            // Calculer le temps écoulé depuis la dernière alerte\r
+            const timeSinceLastAlert = now - rule.last_notified_at_ms;\r
+\r
+            // Si pas assez de temps écoulé, ne pas envoyer\r
+            if (timeSinceLastAlert < rule.cooldown_ms) {\r
+                const remainingMs = rule.cooldown_ms - timeSinceLastAlert;\r
+                const remainingSec = Math.ceil(remainingMs / 1000);\r
+\r
+                console.log(`   ⏳ Cooldown actif`);\r
+                console.log(`      Attendre encore ${remainingSec}s`);\r
+                return false;\r
+            }\r
+        }\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // TOUS LES CRITÈRES SONT OK !\r
+        // ─────────────────────────────────────────────────\r
+        console.log(`   Tous les critères sont remplis`);\r
+        return true;\r
+    },\r
+\r
+    // =====================================================\r
+    // 2. ENVOYER UNE ALERTE VIA LE BON CANAL\r
+    // =====================================================\r
+    /**\r
+     * Envoie une alerte via le canal spécifié dans la règle\r
+     *\r
+     * Cette fonction :\r
+     * - Choisit la bonne fonction d'envoi (email, telegram, etc.)\r
+     * - Prépare les paramètres spécifiques au canal\r
+     * - Gère les erreurs d'envoi\r
+     *\r
+     * @param {Object} rule - Règle d'alerte avec le canal\r
+     * @param {Object} signal - Signal à envoyer\r
+     * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+     */\r
+    async sendViaChannel(rule, signal) {\r
+        console.log(`Envoi via ${rule.channel}...`);\r
+\r
+        let status = 'SENT';\r
+\r
+        try {\r
+            // ─────────────────────────────────────────────\r
+            // SWITCH selon le canal configuré\r
+            // ─────────────────────────────────────────────\r
+            switch (rule.channel) {\r
+\r
+                // ── EMAIL ────────────────────────────────\r
+                case 'EMAIL':\r
+                    // Le destinataire est l'email de l'utilisateur\r
+                    // On l'a récupéré via le JOIN dans la requête SQL\r
+                    await sendAlertEmail(rule.email, signal);\r
+                    break;\r
+\r
+                // ── TELEGRAM ─────────────────────────────\r
+                case 'TELEGRAM':\r
+                    // Le chatId est stocké dans rule.params (JSON)\r
+                    // Exemple de params : {"chatId": "123456789"}\r
+                    const params = JSON.parse(rule.params);\r
+                    const chatId = params.chatId;\r
+\r
+                    if (!chatId) {\r
+                        console.error(' Pas de chatId dans params');\r
+                        status = 'FAILED';\r
+                        break;\r
+                    }\r
+\r
+                    await sendTelegramAlert(chatId, signal);\r
+                    break;\r
+\r
+                // ── DISCORD ──────────────────────────────\r
+                case 'DISCORD':\r
+                    const discordParams = JSON.parse(rule.params);\r
+                    const webhookUrl = discordParams.webhookUrl;\r
+\r
+                    if (!webhookUrl) {\r
+                        console.error(' Pas de webhookUrl dans params');\r
+                        status = 'FAILED';\r
+                        break;\r
+                    }\r
+\r
+                    await sendDiscordAlert(webhookUrl, signal);\r
+                    break;\r
+\r
+                // ── CONSOLE ──────────────────────────────\r
+                case 'CONSOLE':\r
+                    // Pas de paramètres nécessaires\r
+                    await sendConsoleAlert(signal);\r
+                    break;\r
+\r
+                // ── WEB ──────────────────────────────────\r
+                case 'WEB':\r
+                    // Envoyer à l'interface web de l'utilisateur\r
+                    await sendWebAlert(rule.user_id, signal);\r
+                    break;\r
+\r
+                // ── CANAL INCONNU ────────────────────────\r
+                default:\r
+                    console.warn(` Canal inconnu: ${rule.channel}`);\r
+                    status = 'FAILED';\r
+            }\r
+\r
+        } catch (error) {\r
+            // ─────────────────────────────────────────────\r
+            // GESTION DES ERREURS\r
+            // ─────────────────────────────────────────────\r
+            console.error(` Erreur lors de l'envoi via ${rule.channel}:`);\r
+            console.error(`   ${error.message}`);\r
+            status = 'FAILED';\r
+        }\r
+\r
+        return status;\r
+    },\r
+\r
+        // =====================================================\r
+        // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES\r
+        // =====================================================\r
+        /**\r
+         * Fonction principale qui traite un signal crypto\r
+         *\r
+         * WORKFLOW :\r
+         * 1. Récupérer les règles actives pour ce signal\r
+         * 2. Pour chaque règle :\r
+         *    a. Vérifier si alerte doit être envoyée\r
+         *    b. Envoyer via le bon canal\r
+         *    c. Enregistrer dans l'historique\r
+         *    d. Mettre à jour le timestamp\r
+         *\r
+         * @param {Object} signal - Signal crypto reçu\r
+         * @param {string} signal.userId - ID utilisateur\r
+         * @param {number} signal.pairId - ID de la paire\r
+         * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+         * @param {number} signal.confidence - 0-1\r
+         * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+         * @param {string} signal.reason - Explication\r
+         */\r
+        async processSignal(signal) {\r
+            console.log('\n' + '═'.repeat(80));\r
+            console.log(` TRAITEMENT DU SIGNAL`);\r
+            console.log(`   Action : ${signal.action}`);\r
+            console.log(`   Paire  : ${signal.pair}`);\r
+            console.log(`   User   : ${signal.userId}`);\r
+            console.log('═'.repeat(80) + '\n');\r
+\r
+            try {\r
+                // ─────────────────────────────────────────────\r
+                // ÉTAPE 1 : Récupérer les règles actives\r
+                // ─────────────────────────────────────────────\r
+                // Utilise le repository injecté (pas d'import hardcodé)\r
+                const rules = await alertsRepo.findActiveRulesForSignal(\r
+                    signal.userId,\r
+                    signal.pairId\r
+                );\r
+\r
+                if (rules.length === 0) {\r
+                    console.log(' Aucune règle active trouvée pour ce signal');\r
+                    console.log('   → Vérifie que des règles sont configurées en DB\n');\r
+                    return;\r
+                }\r
+\r
+                console.log(` ${rules.length} règle(s) trouvée(s)\n`);\r
+\r
+                // ─────────────────────────────────────────────\r
+                // ÉTAPE 2 : Traiter chaque règle\r
+                // ─────────────────────────────────────────────\r
+                let sentCount = 0;\r
+\r
+                for (const rule of rules) {\r
+                    console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);\r
+\r
+                    // Vérifier si on doit envoyer\r
+                    if (!this.shouldSendAlert(rule, signal)) {\r
+                        console.log(`└─  Alerte NON envoyée\n`);\r
+                        continue;\r
+                    }\r
+\r
+                    // Envoyer via le canal\r
+                    const status = await this.sendViaChannel(rule, signal);\r
+\r
+                    // Préparer les données de l'événement\r
+                    const eventData = {\r
+                        alert_event_id: uuidv4(),\r
+                        rule_id: rule.rule_id,\r
+                        timestamp_ms: Date.now(),\r
+                        severity: signal.criticality,\r
+                        channel: rule.channel,\r
+                        send_status: status,\r
+                        title: `${signal.action} ${signal.pair}`,\r
+                        message: signal.reason,\r
+                        signal_action: signal.action,\r
+                        confidence: signal.confidence,\r
+                        correlation_id: signal.correlationId || null\r
+                    };\r
+\r
+                    // Sauvegarder dans alert_events (via repo injecté)\r
+                    await alertsRepo.saveAlertEvent(eventData);\r
+\r
+                    // Mettre à jour last_notified_at si envoi réussi (via repo injecté)\r
+                    if (status === 'SENT') {\r
+                        await alertsRepo.updateLastNotified(rule.rule_id, Date.now());\r
+                        sentCount++;\r
+                        console.log(`└─  Alerte envoyée avec succès\n`);\r
+                    } else {\r
+                        console.log(`└─  Échec de l'envoi\n`);\r
+                    }\r
+                }\r
+\r
+                // ─────────────────────────────────────────────\r
+                // RÉSUMÉ FINAL\r
+                // ─────────────────────────────────────────────\r
+                console.log('═'.repeat(80));\r
+                console.log(` RÉSUMÉ`);\r
+                console.log(`   Règles vérifiées : ${rules.length}`);\r
+                console.log(`   Alertes envoyées : ${sentCount}`);\r
+                console.log('═'.repeat(80) + '\n');\r
+\r
+            } catch (error) {\r
+                console.error(' ERREUR CRITIQUE dans processSignal:');\r
+                console.error(error);\r
+                throw error; // Remonter l'erreur pour que le controller puisse la gérer\r
+            }\r
+        }\r
+    };\r
+}\r
index c4598714ad19dce248ba508b34172f11a060d4f5..e3077c0ccfdc76c0f2291a7e271e64b20a96e360 100644 (file)
-// =========================================================
-// CANAL CONSOLE - Notifications dans la console
-// =========================================================
-// RÔLE : Afficher les alertes dans le terminal
-// =========================================================
-// UTILITÉ :
-// - Développement et debugging
-// - Tests rapides sans configurer email/telegram
-// - Logs pour surveillance serveur
-// =========================================================
-
-// =========================================================
-// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT)
-// =========================================================
-// Ces codes permettent de colorer le texte dans la console
-const COLORS = {
-    RESET: '\x1b[0m',
-
-    // Couleurs de texte
-    RED: '\x1b[31m',
-    GREEN: '\x1b[32m',
-    YELLOW: '\x1b[33m',
-    BLUE: '\x1b[34m',
-    MAGENTA: '\x1b[35m',
-    CYAN: '\x1b[36m',
-    WHITE: '\x1b[37m',
-
-    // Styles
-    BOLD: '\x1b[1m',
-    DIM: '\x1b[2m',
-
-    // Fond
-    BG_RED: '\x1b[41m',
-    BG_YELLOW: '\x1b[43m',
-    BG_BLUE: '\x1b[44m'
-};
-
-// =========================================================
-// FONCTION PRINCIPALE : Afficher une alerte dans la console
-// =========================================================
-/**
- * Affiche une alerte formatée dans la console
- *
- * @param {Object} signal - Objet contenant les infos du signal
- * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
- * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison de l'alerte
- * @param {string} signal.criticality - CRITICAL, WARNING, INFO
- *
- * @returns {Promise<string>} Toujours 'SENT' (console ne peut pas échouer)
- */
-export async function sendConsoleAlert(signal) {
-
-    // =====================================================
-    // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ
-    // =====================================================
-    let color;
-    let bgColor;
-    let icon;
-
-    switch (signal.criticality) {
-        case 'CRITICAL':
-            color = COLORS.RED;
-            bgColor = COLORS.BG_RED;
-            icon = '🚨';
-            break;
-        case 'WARNING':
-            color = COLORS.YELLOW;
-            bgColor = COLORS.BG_YELLOW;
-            icon = '⚠️';
-            break;
-        case 'INFO':
-        default:
-            color = COLORS.BLUE;
-            bgColor = COLORS.BG_BLUE;
-            icon = 'ℹ️';
-            break;
-    }
-
-    // =====================================================
-    // 2. FORMATER LA CONFIDENCE EN POURCENTAGE
-    // =====================================================
-    const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
-
-    // =====================================================
-    // 3. CRÉER LE TIMESTAMP
-    // =====================================================
-    const timestamp = new Date().toLocaleString('fr-FR', {
-        day: '2-digit',
-        month: '2-digit',
-        year: 'numeric',
-        hour: '2-digit',
-        minute: '2-digit',
-        second: '2-digit'
-    });
-
-    // =====================================================
-    // 4. AFFICHER L'ALERTE AVEC MISE EN FORME
-    // =====================================================
-    console.log('\n'); // Ligne vide avant
-    console.log('═'.repeat(80)); // Ligne de séparation
-
-    // En-tête avec couleur de fond
-    console.log(
-        `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` +
-        `${icon} ALERTE ${signal.criticality} `.padEnd(80) +
-        `${COLORS.RESET}`
-    );
-
-    // Ligne de séparation
-    console.log('─'.repeat(80));
-
-    // Action recommandée (en couleur et gras)
-    console.log(
-        `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` +
-        `${getActionEmoji(signal.action)} ${signal.action}`
-    );
-
-    // Paire crypto
-    console.log(
-        `${COLORS.BOLD}${COLORS.CYAN}PAIRE  :${COLORS.RESET} ` +
-        `${signal.pair}`
-    );
-
-    // Niveau de confiance avec barre visuelle
-    const confidenceBar = createProgressBar(signal.confidence);
-    console.log(
-        `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` +
-        `${confidenceBar} ${confidencePercent}%`
-    );
-
-    // Ligne de séparation
-    console.log('─'.repeat(80));
-
-    // Raison (analyse)
-    console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`);
-    console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères
-
-    // Ligne de séparation
-    console.log('─'.repeat(80));
-
-    // Timestamp en grisé
-    console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`);
-
-    // Ligne de fin
-    console.log('═'.repeat(80));
-    console.log('\n'); // Ligne vide après
-
-    // =====================================================
-    // 5. RETOURNER LE STATUT
-    // =====================================================
-    // La console ne peut jamais échouer, donc toujours SENT
-    return 'SENT';
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Retourne l'emoji correspondant à l'action
- */
-function getActionEmoji(action) {
-    const emojis = {
-        'BUY': '🟢',
-        'SELL': '🔴',
-        'HOLD': '🟡',
-        'STOP_LOSS': '🛑'
-    };
-    return emojis[action] || '📊';
-}
-
-/**
- * Crée une barre de progression visuelle pour la confidence
- * Exemple : ████████░░ (80%)
- */
-function createProgressBar(confidence, length = 20) {
-    const filled = Math.round(confidence * length);
-    const empty = length - filled;
-
-    const bar =
-        COLORS.GREEN + '█'.repeat(filled) +
-        COLORS.DIM + '░'.repeat(empty) +
-        COLORS.RESET;
-
-    return `[${bar}]`;
-}
-
-/**
- * Coupe le texte en plusieurs lignes si trop long
- * Utile pour que la raison ne dépasse pas la largeur du terminal
- */
-function wrapText(text, maxWidth) {
-    const words = text.split(' ');
-    const lines = [];
-    let currentLine = '  '; // Indentation
-
-    words.forEach(word => {
-        if ((currentLine + word).length > maxWidth) {
-            lines.push(currentLine);
-            currentLine = '  ' + word + ' ';
-        } else {
-            currentLine += word + ' ';
-        }
-    });
-
-    if (currentLine.trim()) {
-        lines.push(currentLine);
-    }
-
-    return lines.join('\n');
-}
-
-// =========================================================
-// EXPORT D'UNE FONCTION DE TEST
-// =========================================================
-/**
- * Fonction pour tester rapidement le canal console
- * Usage : node test-console.js
- */
-export function testConsoleAlert() {
-    const fakeSignal = {
-        action: 'BUY',
-        pair: 'BTC/EUR',
-        confidence: 0.87,
-        criticality: 'WARNING',
-        reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.'
-    };
-
-    sendConsoleAlert(fakeSignal);
-}
-
+// =========================================================\r
+// CANAL CONSOLE - Notifications dans la console\r
+// =========================================================\r
+// RÔLE : Afficher les alertes dans le terminal\r
+// =========================================================\r
+// UTILITÉ :\r
+// - Développement et debugging\r
+// - Tests rapides sans configurer email/telegram\r
+// - Logs pour surveillance serveur\r
+// =========================================================\r
+\r
+// =========================================================\r
+// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT)\r
+// =========================================================\r
+// Ces codes permettent de colorer le texte dans la console\r
+const COLORS = {\r
+    RESET: '\x1b[0m',\r
+\r
+    // Couleurs de texte\r
+    RED: '\x1b[31m',\r
+    GREEN: '\x1b[32m',\r
+    YELLOW: '\x1b[33m',\r
+    BLUE: '\x1b[34m',\r
+    MAGENTA: '\x1b[35m',\r
+    CYAN: '\x1b[36m',\r
+    WHITE: '\x1b[37m',\r
+\r
+    // Styles\r
+    BOLD: '\x1b[1m',\r
+    DIM: '\x1b[2m',\r
+\r
+    // Fond\r
+    BG_RED: '\x1b[41m',\r
+    BG_YELLOW: '\x1b[43m',\r
+    BG_BLUE: '\x1b[44m'\r
+};\r
+\r
+// =========================================================\r
+// FONCTION PRINCIPALE : Afficher une alerte dans la console\r
+// =========================================================\r
+/**\r
+ * Affiche une alerte formatée dans la console\r
+ *\r
+ * @param {Object} signal - Objet contenant les infos du signal\r
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison de l'alerte\r
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+ *\r
+ * @returns {Promise<string>} Toujours 'SENT' (console ne peut pas échouer)\r
+ */\r
+export async function sendConsoleAlert(signal) {\r
+\r
+    // =====================================================\r
+    // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ\r
+    // =====================================================\r
+    let color;\r
+    let bgColor;\r
+    let icon;\r
+\r
+    switch (signal.criticality) {\r
+        case 'CRITICAL':\r
+            color = COLORS.RED;\r
+            bgColor = COLORS.BG_RED;\r
+            icon = '🚨';\r
+            break;\r
+        case 'WARNING':\r
+            color = COLORS.YELLOW;\r
+            bgColor = COLORS.BG_YELLOW;\r
+            icon = '⚠️';\r
+            break;\r
+        case 'INFO':\r
+        default:\r
+            color = COLORS.BLUE;\r
+            bgColor = COLORS.BG_BLUE;\r
+            icon = 'ℹ️';\r
+            break;\r
+    }\r
+\r
+    // =====================================================\r
+    // 2. FORMATER LA CONFIDENCE EN POURCENTAGE\r
+    // =====================================================\r
+    const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);\r
+\r
+    // =====================================================\r
+    // 3. CRÉER LE TIMESTAMP\r
+    // =====================================================\r
+    const timestamp = new Date().toLocaleString('fr-FR', {\r
+        day: '2-digit',\r
+        month: '2-digit',\r
+        year: 'numeric',\r
+        hour: '2-digit',\r
+        minute: '2-digit',\r
+        second: '2-digit'\r
+    });\r
+\r
+    // =====================================================\r
+    // 4. AFFICHER L'ALERTE AVEC MISE EN FORME\r
+    // =====================================================\r
+    console.log('\n'); // Ligne vide avant\r
+    console.log('═'.repeat(80)); // Ligne de séparation\r
+\r
+    // En-tête avec couleur de fond\r
+    console.log(\r
+        `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` +\r
+        `${icon} ALERTE ${signal.criticality} `.padEnd(80) +\r
+        `${COLORS.RESET}`\r
+    );\r
+\r
+    // Ligne de séparation\r
+    console.log('─'.repeat(80));\r
+\r
+    // Action recommandée (en couleur et gras)\r
+    console.log(\r
+        `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` +\r
+        `${getActionEmoji(signal.action)} ${signal.action}`\r
+    );\r
+\r
+    // Paire crypto\r
+    console.log(\r
+        `${COLORS.BOLD}${COLORS.CYAN}PAIRE  :${COLORS.RESET} ` +\r
+        `${signal.pair}`\r
+    );\r
+\r
+    // Niveau de confiance avec barre visuelle\r
+    const confidenceBar = createProgressBar(signal.confidence);\r
+    console.log(\r
+        `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` +\r
+        `${confidenceBar} ${confidencePercent}%`\r
+    );\r
+\r
+    // Ligne de séparation\r
+    console.log('─'.repeat(80));\r
+\r
+    // Raison (analyse)\r
+    console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`);\r
+    console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères\r
+\r
+    // Ligne de séparation\r
+    console.log('─'.repeat(80));\r
+\r
+    // Timestamp en grisé\r
+    console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`);\r
+\r
+    // Ligne de fin\r
+    console.log('═'.repeat(80));\r
+    console.log('\n'); // Ligne vide après\r
+\r
+    // =====================================================\r
+    // 5. RETOURNER LE STATUT\r
+    // =====================================================\r
+    // La console ne peut jamais échouer, donc toujours SENT\r
+    return 'SENT';\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Retourne l'emoji correspondant à l'action\r
+ */\r
+function getActionEmoji(action) {\r
+    const emojis = {\r
+        'BUY': '🟢',\r
+        'SELL': '🔴',\r
+        'HOLD': '🟡',\r
+        'STOP_LOSS': '🛑'\r
+    };\r
+    return emojis[action] || '📊';\r
+}\r
+\r
+/**\r
+ * Crée une barre de progression visuelle pour la confidence\r
+ * Exemple : ████████░░ (80%)\r
+ */\r
+function createProgressBar(confidence, length = 20) {\r
+    const filled = Math.round(confidence * length);\r
+    const empty = length - filled;\r
+\r
+    const bar =\r
+        COLORS.GREEN + '█'.repeat(filled) +\r
+        COLORS.DIM + '░'.repeat(empty) +\r
+        COLORS.RESET;\r
+\r
+    return `[${bar}]`;\r
+}\r
+\r
+/**\r
+ * Coupe le texte en plusieurs lignes si trop long\r
+ * Utile pour que la raison ne dépasse pas la largeur du terminal\r
+ */\r
+function wrapText(text, maxWidth) {\r
+    const words = text.split(' ');\r
+    const lines = [];\r
+    let currentLine = '  '; // Indentation\r
+\r
+    words.forEach(word => {\r
+        if ((currentLine + word).length > maxWidth) {\r
+            lines.push(currentLine);\r
+            currentLine = '  ' + word + ' ';\r
+        } else {\r
+            currentLine += word + ' ';\r
+        }\r
+    });\r
+\r
+    if (currentLine.trim()) {\r
+        lines.push(currentLine);\r
+    }\r
+\r
+    return lines.join('\n');\r
+}\r
+\r
+// =========================================================\r
+// EXPORT D'UNE FONCTION DE TEST\r
+// =========================================================\r
+/**\r
+ * Fonction pour tester rapidement le canal console\r
+ * Usage : node test-console.js\r
+ */\r
+export function testConsoleAlert() {\r
+    const fakeSignal = {\r
+        action: 'BUY',\r
+        pair: 'BTC/EUR',\r
+        confidence: 0.87,\r
+        criticality: 'WARNING',\r
+        reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.'\r
+    };\r
+\r
+    sendConsoleAlert(fakeSignal);\r
+}\r
+\r
index f3491d8a167312cb20e89a40438c15dbcc0e0445..a2c9ce83b6f42c9891caa2cb6e1771fb93df5571 100644 (file)
@@ -1,23 +1,23 @@
-// !!! A FAIRE !!!
-
-// =========================================================
-// CANAL DISCORD - Notifications via Discord Webhook
-// =========================================================
-// RÔLE : Envoyer des alertes dans un channel Discord
-// =========================================================
-
-/**
- * Envoie une alerte via Discord Webhook
- *
- * @param {string} webhookUrl - URL du webhook Discord
- * @param {Object} signal - Signal crypto
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendDiscordAlert(webhookUrl, signal) {
-    // TODO: À implémenter si nécessaire
-
-    console.log('💬 Canal Discord pas encore implémenté');
-
-    return 'FAILED';
-
-}
+// !!! A FAIRE !!!\r
+\r
+// =========================================================\r
+// CANAL DISCORD - Notifications via Discord Webhook\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes dans un channel Discord\r
+// =========================================================\r
+\r
+/**\r
+ * Envoie une alerte via Discord Webhook\r
+ *\r
+ * @param {string} webhookUrl - URL du webhook Discord\r
+ * @param {Object} signal - Signal crypto\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendDiscordAlert(webhookUrl, signal) {\r
+    // TODO: À implémenter si nécessaire\r
+\r
+    console.log('💬 Canal Discord pas encore implémenté');\r
+\r
+    return 'FAILED';\r
+\r
+}\r
index 1a4ebeba00adb83d946d93c5ea6612779087a689..9512da6c4834fe30823e36a7cc3502a60b8e3adf 100644 (file)
-// =========================================================
-// CANAL EMAIL - Notifications par email
-// =========================================================
-// RÔLE : Envoyer des alertes par email avec Nodemailer
-// =========================================================
-
-import nodemailer from 'nodemailer';
-
-// =========================================================
-// FONCTION PRINCIPALE : Envoyer une alerte par email
-// =========================================================
-/**
- * Envoie une alerte par email
- *
- * @param {string} to - Adresse email du destinataire
- * @param {Object} signal - Objet contenant les infos du signal
- * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
- * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison de l'alerte
- * @param {string} signal.criticality - CRITICAL, WARNING, INFO
- *
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendAlertEmail(to, signal) {
-
-    // =====================================================
-    // 1. CRÉER LE TRANSPORTEUR SMTP
-    // =====================================================
-    // Le transporteur est l'objet qui envoie les emails
-    // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.)
-    const transporter = nodemailer.createTransport({
-        host: process.env.MAIL_HOST,           // ex: smtp.gmail.com
-        port: parseInt(process.env.MAIL_PORT), // ex: 587
-        secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres
-        auth: {
-            user: process.env.MAIL_USER,       // Ton adresse email
-            pass: process.env.MAIL_PASS        // Ton mot de passe ou App Password
-        }
-    });
-
-    try {
-        // =====================================================
-        // 2. FORMATER LES DONNÉES
-        // =====================================================
-        // Convertir la confidence en pourcentage
-        const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
-
-        // Choisir l'emoji selon l'action
-        const emoji = getEmojiForAction(signal.action);
-
-        // Choisir la couleur selon la criticité
-        const color = getColorForCriticality(signal.criticality);
-
-        // =====================================================
-        // 3. CRÉER LE CONTENU DE L'EMAIL
-        // =====================================================
-        const mailOptions = {
-            from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`,
-            to: to,
-            subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`,
-
-            // Version texte (pour clients email sans HTML)
-            text: `
-🚨 ALERTE CRYPTO 🚨
-
-Action recommandée : ${signal.action}
-Paire : ${signal.pair}
-Confiance : ${confidencePercent}%
-Criticité : ${signal.criticality}
-
-Raison :
-${signal.reason}
-
----
-Wall-e-tte - Votre conseiller crypto
-            `.trim(),
-
-            // Version HTML (plus jolie)
-            html: `
-                <!DOCTYPE html>
-                <html>
-                <head>
-                    <style>
-                        body {
-                            font-family: Arial, sans-serif;
-                            background-color: #f4f4f4;
-                            margin: 0;
-                            padding: 0;
-                        }
-                        .container {
-                            max-width: 600px;
-                            margin: 20px auto;
-                            background-color: white;
-                            border-radius: 10px;
-                            overflow: hidden;
-                            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
-                        }
-                        .header {
-                            background-color: ${color};
-                            color: white;
-                            padding: 20px;
-                            text-align: center;
-                        }
-                        .content {
-                            padding: 30px;
-                        }
-                        .alert-box {
-                            background-color: #f9f9f9;
-                            border-left: 4px solid ${color};
-                            padding: 15px;
-                            margin: 20px 0;
-                        }
-                        .footer {
-                            background-color: #333;
-                            color: white;
-                            text-align: center;
-                            padding: 15px;
-                            font-size: 12px;
-                        }
-                        .stat {
-                            display: inline-block;
-                            margin: 10px 20px;
-                        }
-                        .stat-label {
-                            color: #666;
-                            font-size: 12px;
-                            text-transform: uppercase;
-                        }
-                        .stat-value {
-                            font-size: 24px;
-                            font-weight: bold;
-                            color: ${color};
-                        }
-                    </style>
-                </head>
-                <body>
-                    <div class="container">
-                        <div class="header">
-                            <h1>${emoji} ALERTE ${signal.criticality}</h1>
-                        </div>
-                        <div class="content">
-                            <h2>Recommandation : ${signal.action}</h2>
-                            <p><strong>Paire :</strong> ${signal.pair}</p>
-
-                            <div class="stat">
-                                <div class="stat-label">Confiance</div>
-                                <div class="stat-value">${confidencePercent}%</div>
-                            </div>
-
-                            <div class="alert-box">
-                                <h3>Analyse :</h3>
-                                <p>${signal.reason}</p>
-                            </div>
-
-                            <p style="color: #666; font-size: 12px;">
-                                Timestamp : ${new Date().toLocaleString('fr-FR')}
-                            </p>
-                        </div>
-                        <div class="footer">
-                            Wall-e-tte - Votre conseiller crypto automatisé
-                        </div>
-                    </div>
-                </body>
-                </html>
-            `
-        };
-
-        // =====================================================
-        // 4. ENVOYER L'EMAIL
-        // =====================================================
-        const info = await transporter.sendMail(mailOptions);
-
-        console.log(`Email envoyé avec succès !`);
-        console.log(`   → Destinataire : ${to}`);
-        console.log(`   → Message ID : ${info.messageId}`);
-
-        return 'SENT';
-
-    } catch (error) {
-        // =====================================================
-        // 5. GESTION DES ERREURS
-        // =====================================================
-        console.error('Erreur lors de l\'envoi de l\'email :');
-        console.error(`   → ${error.message}`);
-
-        // Erreurs communes et solutions
-        if (error.message.includes('authentication')) {
-            console.error('   Vérifie MAIL_USER et MAIL_PASS dans .env');
-        } else if (error.message.includes('ECONNREFUSED')) {
-            console.error('   Vérifie MAIL_HOST et MAIL_PORT dans .env');
-        }
-
-        return 'FAILED';
-    }
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Retourne un emoji selon l'action recommandée
- */
-function getEmojiForAction(action) {
-    const emojis = {
-        'BUY': '🟢',
-        'SELL': '🔴',
-        'HOLD': '🟡',
-        'STOP_LOSS': '🛑'
-    };
-    return emojis[action] || '📊';
-}
-
-/**
- * Retourne une couleur selon le niveau de criticité
- */
-function getColorForCriticality(criticality) {
-    const colors = {
-        'CRITICAL': '#dc3545',  // Rouge
-        'WARNING': '#ffc107',   // Orange/Jaune
-        'INFO': '#17a2b8'       // Bleu
-    };
-    return colors[criticality] || '#6c757d';
-}
-
+// =========================================================\r
+// CANAL EMAIL - Notifications par email\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes par email avec Nodemailer\r
+// =========================================================\r
+\r
+import nodemailer from 'nodemailer';\r
+\r
+// =========================================================\r
+// FONCTION PRINCIPALE : Envoyer une alerte par email\r
+// =========================================================\r
+/**\r
+ * Envoie une alerte par email\r
+ *\r
+ * @param {string} to - Adresse email du destinataire\r
+ * @param {Object} signal - Objet contenant les infos du signal\r
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison de l'alerte\r
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+ *\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendAlertEmail(to, signal) {\r
+\r
+    // =====================================================\r
+    // 1. CRÉER LE TRANSPORTEUR SMTP\r
+    // =====================================================\r
+    // Le transporteur est l'objet qui envoie les emails\r
+    // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.)\r
+    const transporter = nodemailer.createTransport({\r
+        host: process.env.MAIL_HOST,           // ex: smtp.gmail.com\r
+        port: parseInt(process.env.MAIL_PORT), // ex: 587\r
+        secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres\r
+        auth: {\r
+            user: process.env.MAIL_USER,       // Ton adresse email\r
+            pass: process.env.MAIL_PASS        // Ton mot de passe ou App Password\r
+        }\r
+    });\r
+\r
+    try {\r
+        // =====================================================\r
+        // 2. FORMATER LES DONNÉES\r
+        // =====================================================\r
+        // Convertir la confidence en pourcentage\r
+        const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);\r
+\r
+        // Choisir l'emoji selon l'action\r
+        const emoji = getEmojiForAction(signal.action);\r
+\r
+        // Choisir la couleur selon la criticité\r
+        const color = getColorForCriticality(signal.criticality);\r
+\r
+        // =====================================================\r
+        // 3. CRÉER LE CONTENU DE L'EMAIL\r
+        // =====================================================\r
+        const mailOptions = {\r
+            from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`,\r
+            to: to,\r
+            subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`,\r
+\r
+            // Version texte (pour clients email sans HTML)\r
+            text: `\r
+🚨 ALERTE CRYPTO 🚨\r
+\r
+Action recommandée : ${signal.action}\r
+Paire : ${signal.pair}\r
+Confiance : ${confidencePercent}%\r
+Criticité : ${signal.criticality}\r
+\r
+Raison :\r
+${signal.reason}\r
+\r
+---\r
+Wall-e-tte - Votre conseiller crypto\r
+            `.trim(),\r
+\r
+            // Version HTML (plus jolie)\r
+            html: `\r
+                <!DOCTYPE html>\r
+                <html>\r
+                <head>\r
+                    <style>\r
+                        body {\r
+                            font-family: Arial, sans-serif;\r
+                            background-color: #f4f4f4;\r
+                            margin: 0;\r
+                            padding: 0;\r
+                        }\r
+                        .container {\r
+                            max-width: 600px;\r
+                            margin: 20px auto;\r
+                            background-color: white;\r
+                            border-radius: 10px;\r
+                            overflow: hidden;\r
+                            box-shadow: 0 4px 6px rgba(0,0,0,0.1);\r
+                        }\r
+                        .header {\r
+                            background-color: ${color};\r
+                            color: white;\r
+                            padding: 20px;\r
+                            text-align: center;\r
+                        }\r
+                        .content {\r
+                            padding: 30px;\r
+                        }\r
+                        .alert-box {\r
+                            background-color: #f9f9f9;\r
+                            border-left: 4px solid ${color};\r
+                            padding: 15px;\r
+                            margin: 20px 0;\r
+                        }\r
+                        .footer {\r
+                            background-color: #333;\r
+                            color: white;\r
+                            text-align: center;\r
+                            padding: 15px;\r
+                            font-size: 12px;\r
+                        }\r
+                        .stat {\r
+                            display: inline-block;\r
+                            margin: 10px 20px;\r
+                        }\r
+                        .stat-label {\r
+                            color: #666;\r
+                            font-size: 12px;\r
+                            text-transform: uppercase;\r
+                        }\r
+                        .stat-value {\r
+                            font-size: 24px;\r
+                            font-weight: bold;\r
+                            color: ${color};\r
+                        }\r
+                    </style>\r
+                </head>\r
+                <body>\r
+                    <div class="container">\r
+                        <div class="header">\r
+                            <h1>${emoji} ALERTE ${signal.criticality}</h1>\r
+                        </div>\r
+                        <div class="content">\r
+                            <h2>Recommandation : ${signal.action}</h2>\r
+                            <p><strong>Paire :</strong> ${signal.pair}</p>\r
+\r
+                            <div class="stat">\r
+                                <div class="stat-label">Confiance</div>\r
+                                <div class="stat-value">${confidencePercent}%</div>\r
+                            </div>\r
+\r
+                            <div class="alert-box">\r
+                                <h3>Analyse :</h3>\r
+                                <p>${signal.reason}</p>\r
+                            </div>\r
+\r
+                            <p style="color: #666; font-size: 12px;">\r
+                                Timestamp : ${new Date().toLocaleString('fr-FR')}\r
+                            </p>\r
+                        </div>\r
+                        <div class="footer">\r
+                            Wall-e-tte - Votre conseiller crypto automatisé\r
+                        </div>\r
+                    </div>\r
+                </body>\r
+                </html>\r
+            `\r
+        };\r
+\r
+        // =====================================================\r
+        // 4. ENVOYER L'EMAIL\r
+        // =====================================================\r
+        const info = await transporter.sendMail(mailOptions);\r
+\r
+        console.log(`Email envoyé avec succès !`);\r
+        console.log(`   → Destinataire : ${to}`);\r
+        console.log(`   → Message ID : ${info.messageId}`);\r
+\r
+        return 'SENT';\r
+\r
+    } catch (error) {\r
+        // =====================================================\r
+        // 5. GESTION DES ERREURS\r
+        // =====================================================\r
+        console.error('Erreur lors de l\'envoi de l\'email :');\r
+        console.error(`   → ${error.message}`);\r
+\r
+        // Erreurs communes et solutions\r
+        if (error.message.includes('authentication')) {\r
+            console.error('   Vérifie MAIL_USER et MAIL_PASS dans .env');\r
+        } else if (error.message.includes('ECONNREFUSED')) {\r
+            console.error('   Vérifie MAIL_HOST et MAIL_PORT dans .env');\r
+        }\r
+\r
+        return 'FAILED';\r
+    }\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Retourne un emoji selon l'action recommandée\r
+ */\r
+function getEmojiForAction(action) {\r
+    const emojis = {\r
+        'BUY': '🟢',\r
+        'SELL': '🔴',\r
+        'HOLD': '🟡',\r
+        'STOP_LOSS': '🛑'\r
+    };\r
+    return emojis[action] || '📊';\r
+}\r
+\r
+/**\r
+ * Retourne une couleur selon le niveau de criticité\r
+ */\r
+function getColorForCriticality(criticality) {\r
+    const colors = {\r
+        'CRITICAL': '#dc3545',  // Rouge\r
+        'WARNING': '#ffc107',   // Orange/Jaune\r
+        'INFO': '#17a2b8'       // Bleu\r
+    };\r
+    return colors[criticality] || '#6c757d';\r
+}\r
+\r
index ce3a74e4c1c1093e9246217a0d86237dfb36cdd7..aa50b3e05d3bfe9e8a7e186f879741ac9ef47297 100644 (file)
@@ -1,55 +1,55 @@
-// !!! À FAIRE !!!
-
-
-// =========================================================
-// CANAL TELEGRAM - Notifications via Telegram Bot
-// =========================================================
-// RÔLE : Envoyer des alertes via un bot Telegram
-// =========================================================
-
-// =========================================================
-// TODO : CONFIGURATION
-// =========================================================
-/*
-    Pour utiliser Telegram, il faut :
-
-    1. Créer un bot Telegram :
-        - Parler à @BotFather sur Telegram
-        - Utiliser /newbot
-        - Récupérer le TOKEN
-
-    2. Récupérer ton CHAT_ID :
-        - Parler à ton bot
-        - Aller sur : https://api.telegram.org/bot<TOKEN>/getUpdates
-        - Récupérer ton chat_id dans le JSON
-
-    3. Ajouter dans .env :
-        TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
-        TELEGRAM_CHAT_ID=123456789
-
-    4. Tester l'envoi :
-        - Utiliser fetch() ou axios
-        - POST vers https://api.telegram.org/bot<TOKEN>/sendMessage
-*/
-
-// =========================================================
-// FONCTION À IMPLÉMENTER
-// =========================================================
-/**
- * Envoie une alerte via Telegram
- *
- * @param {string} chatId - ID du chat Telegram
- * @param {Object} signal - Signal crypto
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendTelegramAlert(chatId, signal) {
-    // TODO: À implémenter ensemble
-
-    console.log('📱 Canal Telegram pas encore implémenté');
-    console.log(`   → chatId: ${chatId}`);
-    console.log(`   → signal: ${signal.action} sur ${signal.pair}`);
-
-    // Pour l'instant, on retourne FAILED
-    return 'FAILED';
-}
-
+// !!! À FAIRE !!!\r
+\r
+\r
+// =========================================================\r
+// CANAL TELEGRAM - Notifications via Telegram Bot\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes via un bot Telegram\r
+// =========================================================\r
+\r
+// =========================================================\r
+// TODO : CONFIGURATION\r
+// =========================================================\r
+/*\r
+    Pour utiliser Telegram, il faut :\r
+\r
+    1. Créer un bot Telegram :\r
+        - Parler à @BotFather sur Telegram\r
+        - Utiliser /newbot\r
+        - Récupérer le TOKEN\r
+\r
+    2. Récupérer ton CHAT_ID :\r
+        - Parler à ton bot\r
+        - Aller sur : https://api.telegram.org/bot<TOKEN>/getUpdates\r
+        - Récupérer ton chat_id dans le JSON\r
+\r
+    3. Ajouter dans .env :\r
+        TELEGRAM_BOT_TOKEN=123456:ABC-DEF...\r
+        TELEGRAM_CHAT_ID=123456789\r
+\r
+    4. Tester l'envoi :\r
+        - Utiliser fetch() ou axios\r
+        - POST vers https://api.telegram.org/bot<TOKEN>/sendMessage\r
+*/\r
+\r
+// =========================================================\r
+// FONCTION À IMPLÉMENTER\r
+// =========================================================\r
+/**\r
+ * Envoie une alerte via Telegram\r
+ *\r
+ * @param {string} chatId - ID du chat Telegram\r
+ * @param {Object} signal - Signal crypto\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendTelegramAlert(chatId, signal) {\r
+    // TODO: À implémenter ensemble\r
+\r
+    console.log('📱 Canal Telegram pas encore implémenté');\r
+    console.log(`   → chatId: ${chatId}`);\r
+    console.log(`   → signal: ${signal.action} sur ${signal.pair}`);\r
+\r
+    // Pour l'instant, on retourne FAILED\r
+    return 'FAILED';\r
+}\r
+\r
index d70c14917000330dae8b54458bb343871c565e22..a882a099fc786b5a61e54811a0a4713bcb9309a7 100644 (file)
@@ -1,76 +1,76 @@
-// =========================================================
-// CANAL WEB - Notifications temps réel via Socket.IO
-// =========================================================
-// RÔLE : Envoyer des alertes en temps réel aux clients
-//        web (Océane) et mobile (Thibaud)
-// =========================================================
-// DÉPENDANCE : socketManager.js doit être initialisé
-// =========================================================
-
-import { sendAlertToUser, isUserConnected } from '../socketManager.js';
-
-/**
- * Envoie une alerte via WebSocket à un utilisateur
- *
- * @param {string} userId - ID de l'utilisateur
- * @param {Object} signal - Signal crypto à envoyer
- * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD'
- * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR')
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison du signal
- * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL'
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- * 
- * @example
- * const status = await sendWebAlert('user-123', {
- *     action: 'BUY',
- *     pair: 'BTC/EUR',
- *     confidence: 0.85,
- *     reason: 'RSI indique une survente',
- *     criticality: 'WARNING'
- * });
- */
-export async function sendWebAlert(userId, signal) {
-    // Vérifier si l'utilisateur est connecté
-    if (!isUserConnected(userId)) {
-        console.log(`⚠️  [Web Channel] User ${userId} non connecté - alerte non envoyée`);
-        return 'FAILED';
-    }
-
-    // Formater l'alerte pour les clients
-    const alertPayload = {
-        // Données du signal
-        action: signal.action,
-        pair: signal.pair,
-        confidence: signal.confidence,
-        reason: signal.reason,
-        
-        // Métadonnées
-        alertLevel: signal.criticality || 'INFO',
-        timestamp: Date.now(),
-        
-        // Prix si disponible
-        ...(signal.priceAtSignal && { price: signal.priceAtSignal })
-    };
-
-    // Envoyer via Socket.IO
-    const sent = sendAlertToUser(userId, alertPayload);
-
-    if (sent) {
-        console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`);
-        return 'SENT';
-    }
-
-    return 'FAILED';
-}
-
-/**
- * Vérifie si le canal web est disponible pour un utilisateur
- * (utile pour le service avant de tenter l'envoi)
- * 
- * @param {string} userId - ID de l'utilisateur
- * @returns {boolean} true si l'utilisateur peut recevoir des alertes web
- */
-export function isWebChannelAvailable(userId) {
-    return isUserConnected(userId);
-}
+// =========================================================\r
+// CANAL WEB - Notifications temps réel via Socket.IO\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes en temps réel aux clients\r
+//        web (Océane) et mobile (Thibaud)\r
+// =========================================================\r
+// DÉPENDANCE : socketManager.js doit être initialisé\r
+// =========================================================\r
+\r
+import { sendAlertToUser, isUserConnected } from '../socketManager.js';\r
+\r
+/**\r
+ * Envoie une alerte via WebSocket à un utilisateur\r
+ *\r
+ * @param {string} userId - ID de l'utilisateur\r
+ * @param {Object} signal - Signal crypto à envoyer\r
+ * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD'\r
+ * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR')\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison du signal\r
+ * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL'\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ * \r
+ * @example\r
+ * const status = await sendWebAlert('user-123', {\r
+ *     action: 'BUY',\r
+ *     pair: 'BTC/EUR',\r
+ *     confidence: 0.85,\r
+ *     reason: 'RSI indique une survente',\r
+ *     criticality: 'WARNING'\r
+ * });\r
+ */\r
+export async function sendWebAlert(userId, signal) {\r
+    // Vérifier si l'utilisateur est connecté\r
+    if (!isUserConnected(userId)) {\r
+        console.log(`⚠️  [Web Channel] User ${userId} non connecté - alerte non envoyée`);\r
+        return 'FAILED';\r
+    }\r
+\r
+    // Formater l'alerte pour les clients\r
+    const alertPayload = {\r
+        // Données du signal\r
+        action: signal.action,\r
+        pair: signal.pair,\r
+        confidence: signal.confidence,\r
+        reason: signal.reason,\r
+        \r
+        // Métadonnées\r
+        alertLevel: signal.criticality || 'INFO',\r
+        timestamp: Date.now(),\r
+        \r
+        // Prix si disponible\r
+        ...(signal.priceAtSignal && { price: signal.priceAtSignal })\r
+    };\r
+\r
+    // Envoyer via Socket.IO\r
+    const sent = sendAlertToUser(userId, alertPayload);\r
+\r
+    if (sent) {\r
+        console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`);\r
+        return 'SENT';\r
+    }\r
+\r
+    return 'FAILED';\r
+}\r
+\r
+/**\r
+ * Vérifie si le canal web est disponible pour un utilisateur\r
+ * (utile pour le service avant de tenter l'envoi)\r
+ * \r
+ * @param {string} userId - ID de l'utilisateur\r
+ * @returns {boolean} true si l'utilisateur peut recevoir des alertes web\r
+ */\r
+export function isWebChannelAvailable(userId) {\r
+    return isUserConnected(userId);\r
+}\r
index 18f74caae50fd8cb90d044c6266cfa45929c63f8..776c227feb6ed98f44a6b1c0d872610006d50d6e 100644 (file)
@@ -1,76 +1,76 @@
-// =========================================================
-// MODULE ALERTS - Point d'entrée
-// =========================================================
-
-// Export des factory functions principales
-export { createAlertsRepo } from './alerts.repo.js';
-export { createAlertsService } from './alerts.service.js';
-export { createAlertsController } from './alerts.controller.js';
-export { createAlertsRouter } from './alerts.router.js';
-
-// Export des adapters (pour différentes bases de données)
-export { createMySQLAdapter } from './adapters/mysql.adapter.js';
-
-// Export du gestionnaire Socket.IO
-export {
-    initSocketIO,
-    sendAlertToUser,
-    broadcastAlert,
-    isUserConnected,
-    getConnectedUsersCount,
-    getIO
-} from './socketManager.js';
-
-// Export des canaux (utiles si on veut les tester séparément)
-export { sendAlertEmail } from './channels/mailer.js';
-export { sendConsoleAlert } from './channels/console.js';
-export { sendTelegramAlert } from './channels/telegram.js';
-export { sendDiscordAlert } from './channels/discord.js';
-export { sendWebAlert } from './channels/web.js';
-
-// =========================================================
-// EXEMPLE D'UTILISATION RAPIDE
-// =========================================================
-/*
-    DANS WALL-E-TTE :
-
-    // 1. Import du module
-    import db from './config/db.js';
-    import {
-        createAlertsRepo,
-        createAlertsService,
-        createAlertsController,
-        createAlertsRouter
-    } from './modules/alerts/index.js';
-
-    // 2. Initialiser le module (dans l'ordre !)
-    const alertsRepo = createAlertsRepo(db);
-    const alertsService = createAlertsService(alertsRepo);
-    const alertsController = createAlertsController(alertsService, alertsRepo);
-    const alertsRouter = createAlertsRouter(alertsController);
-
-    // 3. Utiliser dans Express
-    app.use('/api/alerts', alertsRouter);
-
-    // 4. Ou utiliser directement le service
-    await alertsService.processSignal(mySignal);
-
-    ═══════════════════════════════════════════════════════
-
-    DANS UN AUTRE PROJET :
-
-    // Exactement pareil !
-    import myDb from './my-db-config.js';
-    import {
-        createAlertsRepo,
-        createAlertsService
-    } from '@wall-e-tte/alerts-module';
-
-    const repo = createAlertsRepo(myDb, {
-        alertsTable: 'my_notifications',
-        usersTable: 'my_users'
-    });
-    const service = createAlertsService(repo);
-
-    await service.processSignal(signal);  // Ça marche !
-*/
+// =========================================================\r
+// MODULE ALERTS - Point d'entrée\r
+// =========================================================\r
+\r
+// Export des factory functions principales\r
+export { createAlertsRepo } from './alerts.repo.js';\r
+export { createAlertsService } from './alerts.service.js';\r
+export { createAlertsController } from './alerts.controller.js';\r
+export { createAlertsRouter } from './alerts.router.js';\r
+\r
+// Export des adapters (pour différentes bases de données)\r
+export { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+\r
+// Export du gestionnaire Socket.IO\r
+export {\r
+    initSocketIO,\r
+    sendAlertToUser,\r
+    broadcastAlert,\r
+    isUserConnected,\r
+    getConnectedUsersCount,\r
+    getIO\r
+} from './socketManager.js';\r
+\r
+// Export des canaux (utiles si on veut les tester séparément)\r
+export { sendAlertEmail } from './channels/mailer.js';\r
+export { sendConsoleAlert } from './channels/console.js';\r
+export { sendTelegramAlert } from './channels/telegram.js';\r
+export { sendDiscordAlert } from './channels/discord.js';\r
+export { sendWebAlert } from './channels/web.js';\r
+\r
+// =========================================================\r
+// EXEMPLE D'UTILISATION RAPIDE\r
+// =========================================================\r
+/*\r
+    DANS WALL-E-TTE :\r
+\r
+    // 1. Import du module\r
+    import db from './config/db.js';\r
+    import {\r
+        createAlertsRepo,\r
+        createAlertsService,\r
+        createAlertsController,\r
+        createAlertsRouter\r
+    } from './modules/alerts/index.js';\r
+\r
+    // 2. Initialiser le module (dans l'ordre !)\r
+    const alertsRepo = createAlertsRepo(db);\r
+    const alertsService = createAlertsService(alertsRepo);\r
+    const alertsController = createAlertsController(alertsService, alertsRepo);\r
+    const alertsRouter = createAlertsRouter(alertsController);\r
+\r
+    // 3. Utiliser dans Express\r
+    app.use('/api/alerts', alertsRouter);\r
+\r
+    // 4. Ou utiliser directement le service\r
+    await alertsService.processSignal(mySignal);\r
+\r
+    ═══════════════════════════════════════════════════════\r
+\r
+    DANS UN AUTRE PROJET :\r
+\r
+    // Exactement pareil !\r
+    import myDb from './my-db-config.js';\r
+    import {\r
+        createAlertsRepo,\r
+        createAlertsService\r
+    } from '@wall-e-tte/alerts-module';\r
+\r
+    const repo = createAlertsRepo(myDb, {\r
+        alertsTable: 'my_notifications',\r
+        usersTable: 'my_users'\r
+    });\r
+    const service = createAlertsService(repo);\r
+\r
+    await service.processSignal(signal);  // Ça marche !\r
+*/\r
index ef6ff14333e9b162c3dc8d2839a756527abc6342..931ed756ed0658ea31856b9bdd472d6594fc1331 100644 (file)
-// =========================================================
-// SOCKET MANAGER - Gestion des connexions WebSocket
-// =========================================================
-// RÔLE : Centraliser la gestion des connexions Socket.IO
-//        pour le module d'alertes
-// =========================================================
-// UTILISÉ PAR : web.js
-// =========================================================
-
-import { Server } from 'socket.io';
-
-// =========================================================
-// STOCKAGE DES CONNEXIONS
-// =========================================================
-// Map : userId → socket
-// Permet d'envoyer une alerte à un utilisateur spécifique
-const userSockets = new Map();
-
-// Instance du serveur Socket.IO
-let io = null;
-
-// =========================================================
-// INITIALISATION DU SERVEUR SOCKET.IO
-// =========================================================
-/**
- * Initialise Socket.IO sur un serveur HTTP existant
- *
- * @param {Object} httpServer - Serveur HTTP (Express)
- * @param {Object} options - Options Socket.IO (optionnel)
- * @returns {Server} Instance Socket.IO
- *
- * @example
- * // Dans le fichier principal du serveur (index.js)
- * import express from 'express';
- * import { createServer } from 'http';
- * import { initSocketIO } from './modules/alerts/socketManager.js';
- *
- * const app = express();
- * const httpServer = createServer(app);
- * const io = initSocketIO(httpServer);
- *
- * httpServer.listen(3000);
- */
-export function initSocketIO(httpServer, options = {}) {
-    // Configuration par défaut + options personnalisées
-    const defaultOptions = {
-        cors: {
-            origin: "*", // En prod, restreindre aux domaines autorisés
-            methods: ["GET", "POST"]
-        }
-    };
-
-    io = new Server(httpServer, { ...defaultOptions, ...options });
-
-    // ─────────────────────────────────────────────────────
-    // GESTION DES CONNEXIONS
-    // ─────────────────────────────────────────────────────
-    io.on('connection', (socket) => {
-        console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`);
-
-        // ─────────────────────────────────────────────────
-        // EVENT : Authentification de l'utilisateur
-        // ─────────────────────────────────────────────────
-        // Le client doit envoyer son userId après connexion
-        socket.on('auth', (userId) => {
-            if (!userId) {
-                console.log(`  [Socket.IO] Auth sans userId (socket: ${socket.id})`);
-                return;
-            }
-
-            // Stocker la correspondance userId → socket
-            userSockets.set(userId, socket);
-            socket.userId = userId; // Garder une référence sur le socket
-
-            console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);
-            console.log(`   Utilisateurs connectés : ${userSockets.size}`);
-
-            // Confirmer l'authentification au client
-            socket.emit('auth_success', {
-                userId,
-                message: 'Connecté au système d\'alertes'
-            });
-        });
-
-        // ─────────────────────────────────────────────────
-        // EVENT : Déconnexion
-        // ─────────────────────────────────────────────────
-        socket.on('disconnect', (reason) => {
-            if (socket.userId) {
-                userSockets.delete(socket.userId);
-                console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`);
-                console.log(`   Utilisateurs connectés : ${userSockets.size}`);
-            }
-        });
-
-        // ─────────────────────────────────────────────────
-        // EVENT : Ping (test de connexion)
-        // ─────────────────────────────────────────────────
-        socket.on('ping_alerts', () => {
-            socket.emit('pong_alerts', {
-                timestamp: Date.now(),
-                status: 'ok'
-            });
-        });
-    });
-
-    console.log(' [Socket.IO] Serveur initialisé');
-    return io;
-}
-
-// =========================================================
-// FONCTIONS D'ENVOI D'ALERTES
-// =========================================================
-
-/**
- * Envoie une alerte à un utilisateur spécifique
- *
- * @param {string} userId - ID de l'utilisateur cible
- * @param {Object} alertData - Données de l'alerte
- * @returns {boolean} true si envoyé, false si utilisateur non connecté
- */
-export function sendAlertToUser(userId, alertData) {
-    const socket = userSockets.get(userId);
-
-    if (socket) {
-        socket.emit('alert', {
-            ...alertData,
-            timestamp: Date.now()
-        });
-        console.log(` [Socket.IO] Alerte envoyée à ${userId}`);
-        return true;
-    }
-
-    console.log(`  [Socket.IO] Utilisateur ${userId} non connecté`);
-    return false;
-}
-
-/**
- * Envoie une alerte à tous les utilisateurs connectés
- *
- * @param {Object} alertData - Données de l'alerte
- * @returns {number} Nombre d'utilisateurs notifiés
- */
-export function broadcastAlert(alertData) {
-    if (!io) {
-        console.error(' [Socket.IO] Serveur non initialisé');
-        return 0;
-    }
-
-    io.emit('alert', {
-        ...alertData,
-        timestamp: Date.now()
-    });
-
-    console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);
-    return userSockets.size;
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Vérifie si un utilisateur est connecté
- * @param {string} userId
- * @returns {boolean}
- */
-export function isUserConnected(userId) {
-    return userSockets.has(userId);
-}
-
-/**
- * Retourne le nombre d'utilisateurs connectés
- * @returns {number}
- */
-export function getConnectedUsersCount() {
-    return userSockets.size;
-}
-
-/**
- * Retourne la liste des userIds connectés
- * @returns {string[]}
- */
-export function getConnectedUserIds() {
-    return Array.from(userSockets.keys());
-}
-
-/**
- * Retourne l'instance Socket.IO (pour usage avancé)
- * @returns {Server|null}
- */
-export function getIO() {
-    return io;
-}
+// =========================================================\r
+// SOCKET MANAGER - Gestion des connexions WebSocket\r
+// =========================================================\r
+// RÔLE : Centraliser la gestion des connexions Socket.IO\r
+//        pour le module d'alertes\r
+// =========================================================\r
+// UTILISÉ PAR : web.js\r
+// =========================================================\r
+\r
+import { Server } from 'socket.io';\r
+\r
+// =========================================================\r
+// STOCKAGE DES CONNEXIONS\r
+// =========================================================\r
+// Map : userId → socket\r
+// Permet d'envoyer une alerte à un utilisateur spécifique\r
+const userSockets = new Map();\r
+\r
+// Instance du serveur Socket.IO\r
+let io = null;\r
+\r
+// =========================================================\r
+// INITIALISATION DU SERVEUR SOCKET.IO\r
+// =========================================================\r
+/**\r
+ * Initialise Socket.IO sur un serveur HTTP existant\r
+ *\r
+ * @param {Object} httpServer - Serveur HTTP (Express)\r
+ * @param {Object} options - Options Socket.IO (optionnel)\r
+ * @returns {Server} Instance Socket.IO\r
+ *\r
+ * @example\r
+ * // Dans le fichier principal du serveur (index.js)\r
+ * import express from 'express';\r
+ * import { createServer } from 'http';\r
+ * import { initSocketIO } from './modules/alerts/socketManager.js';\r
+ *\r
+ * const app = express();\r
+ * const httpServer = createServer(app);\r
+ * const io = initSocketIO(httpServer);\r
+ *\r
+ * httpServer.listen(3000);\r
+ */\r
+export function initSocketIO(httpServer, options = {}) {\r
+    // Configuration par défaut + options personnalisées\r
+    const defaultOptions = {\r
+        cors: {\r
+            origin: "*", // En prod, restreindre aux domaines autorisés\r
+            methods: ["GET", "POST"]\r
+        }\r
+    };\r
+\r
+    io = new Server(httpServer, { ...defaultOptions, ...options });\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // GESTION DES CONNEXIONS\r
+    // ─────────────────────────────────────────────────────\r
+    io.on('connection', (socket) => {\r
+        console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`);\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // EVENT : Authentification de l'utilisateur\r
+        // ─────────────────────────────────────────────────\r
+        // Le client doit envoyer son userId après connexion\r
+        socket.on('auth', (userId) => {\r
+            if (!userId) {\r
+                console.log(`  [Socket.IO] Auth sans userId (socket: ${socket.id})`);\r
+                return;\r
+            }\r
+\r
+            // Stocker la correspondance userId → socket\r
+            userSockets.set(userId, socket);\r
+            socket.userId = userId; // Garder une référence sur le socket\r
+\r
+            console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);\r
+            console.log(`   Utilisateurs connectés : ${userSockets.size}`);\r
+\r
+            // Confirmer l'authentification au client\r
+            socket.emit('auth_success', {\r
+                userId,\r
+                message: 'Connecté au système d\'alertes'\r
+            });\r
+        });\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // EVENT : Déconnexion\r
+        // ─────────────────────────────────────────────────\r
+        socket.on('disconnect', (reason) => {\r
+            if (socket.userId) {\r
+                userSockets.delete(socket.userId);\r
+                console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`);\r
+                console.log(`   Utilisateurs connectés : ${userSockets.size}`);\r
+            }\r
+        });\r
+\r
+        // ─────────────────────────────────────────────────\r
+        // EVENT : Ping (test de connexion)\r
+        // ─────────────────────────────────────────────────\r
+        socket.on('ping_alerts', () => {\r
+            socket.emit('pong_alerts', {\r
+                timestamp: Date.now(),\r
+                status: 'ok'\r
+            });\r
+        });\r
+    });\r
+\r
+    console.log(' [Socket.IO] Serveur initialisé');\r
+    return io;\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS D'ENVOI D'ALERTES\r
+// =========================================================\r
+\r
+/**\r
+ * Envoie une alerte à un utilisateur spécifique\r
+ *\r
+ * @param {string} userId - ID de l'utilisateur cible\r
+ * @param {Object} alertData - Données de l'alerte\r
+ * @returns {boolean} true si envoyé, false si utilisateur non connecté\r
+ */\r
+export function sendAlertToUser(userId, alertData) {\r
+    const socket = userSockets.get(userId);\r
+\r
+    if (socket) {\r
+        socket.emit('alert', {\r
+            ...alertData,\r
+            timestamp: Date.now()\r
+        });\r
+        console.log(` [Socket.IO] Alerte envoyée à ${userId}`);\r
+        return true;\r
+    }\r
+\r
+    console.log(`  [Socket.IO] Utilisateur ${userId} non connecté`);\r
+    return false;\r
+}\r
+\r
+/**\r
+ * Envoie une alerte à tous les utilisateurs connectés\r
+ *\r
+ * @param {Object} alertData - Données de l'alerte\r
+ * @returns {number} Nombre d'utilisateurs notifiés\r
+ */\r
+export function broadcastAlert(alertData) {\r
+    if (!io) {\r
+        console.error(' [Socket.IO] Serveur non initialisé');\r
+        return 0;\r
+    }\r
+\r
+    io.emit('alert', {\r
+        ...alertData,\r
+        timestamp: Date.now()\r
+    });\r
+\r
+    console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);\r
+    return userSockets.size;\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Vérifie si un utilisateur est connecté\r
+ * @param {string} userId\r
+ * @returns {boolean}\r
+ */\r
+export function isUserConnected(userId) {\r
+    return userSockets.has(userId);\r
+}\r
+\r
+/**\r
+ * Retourne le nombre d'utilisateurs connectés\r
+ * @returns {number}\r
+ */\r
+export function getConnectedUsersCount() {\r
+    return userSockets.size;\r
+}\r
+\r
+/**\r
+ * Retourne la liste des userIds connectés\r
+ * @returns {string[]}\r
+ */\r
+export function getConnectedUserIds() {\r
+    return Array.from(userSockets.keys());\r
+}\r
+\r
+/**\r
+ * Retourne l'instance Socket.IO (pour usage avancé)\r
+ * @returns {Server|null}\r
+ */\r
+export function getIO() {\r
+    return io;\r
+}\r
index 0ad76cc842ea41ccf9f4a5ba6dd16a793022e340..220af6f4ec4590446d6c3f1265c86d301a798b71 100644 (file)
-// =========================================================
-// SCRIPT DE TEST - Module Alerts RÉUTILISABLE
-// =========================================================
-// Ce script teste ton module d'alertes avec la nouvelle
-// architecture réutilisable (injection de dépendances)
-// =========================================================
-// USAGE : node test-alerts.js
-// =========================================================
-
-import dotenv from 'dotenv';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-// Charger les variables d'environnement
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-dotenv.config({ path: path.resolve(__dirname, '.env') });
-
-// =========================================================
-// INITIALISATION DU MODULE
-// =========================================================
-import db from './config/db.js';
-import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
-import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
-import { createAlertsService } from './modules/alerts/alerts.service.js';
-
-// 1. Créer l'adapter MySQL avec la connexion DB
-const mysqlAdapter = createMySQLAdapter(db);
-
-// 2. Créer le repository avec l'adapter
-const alertsRepo = createAlertsRepo(mysqlAdapter);
-
-// 3. Créer le service avec le repository
-const alertsService = createAlertsService(alertsRepo);
-
-// =========================================================
-// FONCTION DE TEST PRINCIPALE
-// =========================================================
-async function runTest() {
-    console.log('\n' + '═'.repeat(80));
-    console.log('TEST DU MODULE ALERTS');
-    console.log('═'.repeat(80) + '\n');
-
-    try {
-        // =========================================================
-        // ÉTAPE 1 : Vérifier la connexion DB
-        // =========================================================
-        console.log('Test de connexion à la base de données\n');
-
-        // Utilise le repository créé avec injection de dépendances
-        const rules = await alertsRepo.findActiveRulesForSignal('test', 1);
-        console.log(`Connexion DB OK`);
-        console.log(`   Règles trouvées : ${rules.length}\n`);
-
-        if (rules.length === 0) {
-            console.log('ATTENTION : Aucune règle trouvée en DB');
-        }
-
-        // =========================================================
-        // ÉTAPE 2 : Test du canal CONSOLE
-        // =========================================================
-        console.log('─'.repeat(80));
-        console.log('Test du canal CONSOLE\n');
-
-        const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
-
-        const testSignalConsole = {
-            action: 'BUY',
-            pair: 'BTC/EUR',
-            confidence: 0.87,
-            criticality: 'WARNING',
-            reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'
-        };
-
-        await sendConsoleAlert(testSignalConsole);
-        console.log('Test CONSOLE réussi\n');
-
-        // =========================================================
-        // ÉTAPE 3 : Test du canal EMAIL
-        // =========================================================
-        console.log('─'.repeat(80));
-        console.log('Test du canal EMAIL\n');
-
-        if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {
-            console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');
-            console.log('   Pour tester configurez vos variables dans .env\n');
-        } else {
-            console.log('Configuration email détectée');
-            console.log(`   Host : ${process.env.MAIL_HOST}`);
-            console.log(`   User : ${process.env.MAIL_USER}`);
-            console.log('   Envoi d\'un email de test...\n');
-
-            const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');
-
-            const testSignalEmail = {
-                action: 'SELL',
-                pair: 'ETH/EUR',
-                confidence: 0.92,
-                criticality: 'CRITICAL',
-                reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'
-            };
-
-            const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail);
-
-            if (status === 'SENT') {
-                console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n');
-            } else {
-                console.log('Test EMAIL échoué. Vérifiez votre configuration.\n');
-            }
-        }
-
-        // =========================================================
-        // ÉTAPE 4 : Test du service complet
-        // =========================================================
-        if (rules.length > 0) {
-            console.log('─'.repeat(80));
-            console.log('Test du service complet\n');
-
-            const testSignalService = {
-                userId: rules[0].user_id,
-                pairId: rules[0].pair_id || 1,
-                pair: 'BTC/EUR',
-                action: 'BUY',
-                confidence: 0.85,
-                criticality: 'INFO',
-                reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',
-                priceAtSignal: 45000.50
-            };
-
-            // Utilise le service créé avec injection de dépendances
-            await alertsService.processSignal(testSignalService);
-            console.log('Test SERVICE complet réussi !\n');
-        }
-
-        // =========================================================
-        // RÉSUMÉ FINAL
-        // =========================================================
-        console.log('═'.repeat(80));
-        console.log('TOUS LES TESTS SONT TERMINÉS');
-        console.log('═'.repeat(80));
-        console.log('\n RÉSUMÉ :');
-        console.log('   Connexion DB');
-        console.log('   Canal CONSOLE');
-        console.log(`   ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);
-        console.log(`   ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);
-
-    } catch (error) {
-        console.error('\n ERREUR LORS DU TEST :');
-        console.error(error);
-        console.error('\n VÉRIFICATIONS :');
-        console.error('   - Le fichier .env existe et contient les bonnes valeurs ?');
-        console.error('   - La base de données est démarrée ?');
-        console.error('   - Les tables sont créées (schema.sql) ?');
-        console.error('\n');
-    } finally {
-        // Quitter proprement
-        process.exit();
-    }
-}
-
-// =========================================================
-// LANCEMENT DU TEST
-// =========================================================
-runTest();
+// =========================================================\r
+// SCRIPT DE TEST - Module Alerts RÉUTILISABLE\r
+// =========================================================\r
+// Ce script teste ton module d'alertes avec la nouvelle\r
+// architecture réutilisable (injection de dépendances)\r
+// =========================================================\r
+// USAGE : node test-alerts.js\r
+// =========================================================\r
+\r
+import dotenv from 'dotenv';\r
+import path from 'path';\r
+import { fileURLToPath } from 'url';\r
+\r
+// Charger les variables d'environnement\r
+const __filename = fileURLToPath(import.meta.url);\r
+const __dirname = path.dirname(__filename);\r
+dotenv.config({ path: path.resolve(__dirname, '.env') });\r
+\r
+// =========================================================\r
+// INITIALISATION DU MODULE\r
+// =========================================================\r
+import db from './config/db.js';\r
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';\r
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';\r
+import { createAlertsService } from './modules/alerts/alerts.service.js';\r
+\r
+// 1. Créer l'adapter MySQL avec la connexion DB\r
+const mysqlAdapter = createMySQLAdapter(db);\r
+\r
+// 2. Créer le repository avec l'adapter\r
+const alertsRepo = createAlertsRepo(mysqlAdapter);\r
+\r
+// 3. Créer le service avec le repository\r
+const alertsService = createAlertsService(alertsRepo);\r
+\r
+// =========================================================\r
+// FONCTION DE TEST PRINCIPALE\r
+// =========================================================\r
+async function runTest() {\r
+    console.log('\n' + '═'.repeat(80));\r
+    console.log('TEST DU MODULE ALERTS');\r
+    console.log('═'.repeat(80) + '\n');\r
+\r
+    try {\r
+        // =========================================================\r
+        // ÉTAPE 1 : Vérifier la connexion DB\r
+        // =========================================================\r
+        console.log('Test de connexion à la base de données\n');\r
+\r
+        // Utilise le repository créé avec injection de dépendances\r
+        const rules = await alertsRepo.findActiveRulesForSignal('test', 1);\r
+        console.log(`Connexion DB OK`);\r
+        console.log(`   Règles trouvées : ${rules.length}\n`);\r
+\r
+        if (rules.length === 0) {\r
+            console.log('ATTENTION : Aucune règle trouvée en DB');\r
+        }\r
+\r
+        // =========================================================\r
+        // ÉTAPE 2 : Test du canal CONSOLE\r
+        // =========================================================\r
+        console.log('─'.repeat(80));\r
+        console.log('Test du canal CONSOLE\n');\r
+\r
+        const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');\r
+\r
+        const testSignalConsole = {\r
+            action: 'BUY',\r
+            pair: 'BTC/EUR',\r
+            confidence: 0.87,\r
+            criticality: 'WARNING',\r
+            reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'\r
+        };\r
+\r
+        await sendConsoleAlert(testSignalConsole);\r
+        console.log('Test CONSOLE réussi\n');\r
+\r
+        // =========================================================\r
+        // ÉTAPE 3 : Test du canal EMAIL\r
+        // =========================================================\r
+        console.log('─'.repeat(80));\r
+        console.log('Test du canal EMAIL\n');\r
+\r
+        if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {\r
+            console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');\r
+            console.log('   Pour tester configurez vos variables dans .env\n');\r
+        } else {\r
+            console.log('Configuration email détectée');\r
+            console.log(`   Host : ${process.env.MAIL_HOST}`);\r
+            console.log(`   User : ${process.env.MAIL_USER}`);\r
+            console.log('   Envoi d\'un email de test...\n');\r
+\r
+            const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');\r
+\r
+            const testSignalEmail = {\r
+                action: 'SELL',\r
+                pair: 'ETH/EUR',\r
+                confidence: 0.92,\r
+                criticality: 'CRITICAL',\r
+                reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'\r
+            };\r
+\r
+            const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail);\r
+\r
+            if (status === 'SENT') {\r
+                console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n');\r
+            } else {\r
+                console.log('Test EMAIL échoué. Vérifiez votre configuration.\n');\r
+            }\r
+        }\r
+\r
+        // =========================================================\r
+        // ÉTAPE 4 : Test du service complet\r
+        // =========================================================\r
+        if (rules.length > 0) {\r
+            console.log('─'.repeat(80));\r
+            console.log('Test du service complet\n');\r
+\r
+            const testSignalService = {\r
+                userId: rules[0].user_id,\r
+                pairId: rules[0].pair_id || 1,\r
+                pair: 'BTC/EUR',\r
+                action: 'BUY',\r
+                confidence: 0.85,\r
+                criticality: 'INFO',\r
+                reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',\r
+                priceAtSignal: 45000.50\r
+            };\r
+\r
+            // Utilise le service créé avec injection de dépendances\r
+            await alertsService.processSignal(testSignalService);\r
+            console.log('Test SERVICE complet réussi !\n');\r
+        }\r
+\r
+        // =========================================================\r
+        // RÉSUMÉ FINAL\r
+        // =========================================================\r
+        console.log('═'.repeat(80));\r
+        console.log('TOUS LES TESTS SONT TERMINÉS');\r
+        console.log('═'.repeat(80));\r
+        console.log('\n RÉSUMÉ :');\r
+        console.log('   Connexion DB');\r
+        console.log('   Canal CONSOLE');\r
+        console.log(`   ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);\r
+        console.log(`   ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);\r
+\r
+    } catch (error) {\r
+        console.error('\n ERREUR LORS DU TEST :');\r
+        console.error(error);\r
+        console.error('\n VÉRIFICATIONS :');\r
+        console.error('   - Le fichier .env existe et contient les bonnes valeurs ?');\r
+        console.error('   - La base de données est démarrée ?');\r
+        console.error('   - Les tables sont créées (schema.sql) ?');\r
+        console.error('\n');\r
+    } finally {\r
+        // Quitter proprement\r
+        process.exit();\r
+    }\r
+}\r
+\r
+// =========================================================\r
+// LANCEMENT DU TEST\r
+// =========================================================\r
+runTest();\r
index 4f927b92a388c0f8fd74c3fa407f9db46e8562fd..8e78b7a8fe10c7d4eba1fa8eb36dad3001913ff1 100644 (file)
-// =========================================================
-// INITIALISATION DU MODULE ALERTS
-// =========================================================
-// Ce fichier montre comment initialiser et utiliser
-// le module d'alertes dans Wall-e-tte
-// =========================================================
-
-import db from '../config/db.js';
-import {
-    createAlertsController,
-    createAlertsRepo,
-    createAlertsRouter,
-    createAlertsService,
-    createMySQLAdapter
-} from './alerts/index.js';
-
-// =========================================================
-// FONCTION D'INITIALISATION
-// =========================================================
-/**
- * Initialise tout le module d'alertes
- *
- * @param {Object} dbConnection - Connexion à la base de données
- * @param {Object} options - Options de configuration
- * @returns {Object} { repo, service, controller, router }
- */
-export function initializeAlertsModule(dbConnection = db, options = {}) {
-
-    console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne
-    console.log('INITIALISATION DU MODULE ALERTS');
-    console.log('═'.repeat(80) + '\n');
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 1 : Créer l'Adapter MySQL
-    // ─────────────────────────────────────────────────────
-    // L'adapter contient les requêtes SQL spécifiques
-    const adapter = createMySQLAdapter(dbConnection, {
-        alertsTable: options.alertsTable || 'alert_rules',
-        usersTable: options.usersTable || 'users',
-        eventsTable: options.eventsTable || 'alert_events'
-    });
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 2 : Créer le Repository
-    // ─────────────────────────────────────────────────────
-    // Le repository gère les requêtes SQL
-    const alertsRepo = createAlertsRepo(adapter, {
-        alertsTable: options.alertsTable || 'alert_rules',
-        usersTable: options.usersTable || 'users',
-        eventsTable: options.eventsTable || 'alert_events'
-    });
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 3 : Créer le Service
-    // ─────────────────────────────────────────────────────
-    // Le service contient la logique métier
-    const alertsService = createAlertsService(alertsRepo, {
-        defaultCooldown: options.defaultCooldown || 60000
-    });
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 4 : Créer le Controller
-    // ─────────────────────────────────────────────────────
-    // Le controller gère les requêtes HTTP
-    // (Plus besoin de db car le CRUD passe par le repo)
-    const alertsController = createAlertsController(alertsService, alertsRepo);
-
-    // ─────────────────────────────────────────────────────
-    // ÉTAPE 5 : Créer le Router
-    // ─────────────────────────────────────────────────────
-    // Le router définit les routes API
-    const alertsRouter = createAlertsRouter(alertsController);
-
-    console.log('Module Alerts complètement initialisé !');
-    console.log('═'.repeat(80) + '\n');
-
-    // ─────────────────────────────────────────────────────
-    // RETOURNER TOUT
-    // ─────────────────────────────────────────────────────
-    return {
-        adapter,
-        repo: alertsRepo,
-        service: alertsService,
-        controller: alertsController,
-        router: alertsRouter
-    };
-}
-
-// =========================================================
-// EXPORT PAR DÉFAUT
-// =========================================================
-// Pour Wall-e-tte, initialiser avec la DB par défaut
-export default initializeAlertsModule();
-
-// =========================================================
-// EXEMPLE D'UTILISATION
-// =========================================================
-/*
-    UTILISATION SIMPLE (dans Wall-e-tte) :
-
-    // Option 1 : Import par défaut (utilise la DB par défaut)
-    import alerts from './modules/init-alerts.js';
-
-    app.use('/api/alerts', alerts.router);
-    await alerts.service.processSignal(signal);
-
-================================================================================
-
-    // Option 2 : Import avec configuration personnalisée
-    import { initializeAlertsModule } from './modules/init-alerts.js';
-    import myDb from './my-db.js';
-
-    const alerts = initializeAlertsModule(myDb, {
-        alertsTable: 'mes_alertes',
-        defaultCooldown: 120000  // 2 minutes
-    });
-
-    app.use('/api/alerts', alerts.router);
-
-================================================================================
-
-    UTILISATION DANS UN AUTRE PROJET :
-
-    // Copier ce fichier et changer juste l'import de 'db'
-    import strangerDb from './stranger-db-config.js';
-    import {
-        createAlertsRepo,
-        createAlertsService
-    } from '@wall-e-tte/alerts';
-
-    const repo = createAlertsRepo(strangerDb);
-    const service = createAlertsService(repo);
-
-*/
+// =========================================================\r
+// INITIALISATION DU MODULE ALERTS\r
+// =========================================================\r
+// Ce fichier montre comment initialiser et utiliser\r
+// le module d'alertes dans Wall-e-tte\r
+// =========================================================\r
+\r
+import db from '../config/db.js';\r
+import {\r
+    createAlertsController,\r
+    createAlertsRepo,\r
+    createAlertsRouter,\r
+    createAlertsService,\r
+    createMySQLAdapter\r
+} from './alerts/index.js';\r
+\r
+// =========================================================\r
+// FONCTION D'INITIALISATION\r
+// =========================================================\r
+/**\r
+ * Initialise tout le module d'alertes\r
+ *\r
+ * @param {Object} dbConnection - Connexion à la base de données\r
+ * @param {Object} options - Options de configuration\r
+ * @returns {Object} { repo, service, controller, router }\r
+ */\r
+export function initializeAlertsModule(dbConnection = db, options = {}) {\r
+\r
+    console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne\r
+    console.log('INITIALISATION DU MODULE ALERTS');\r
+    console.log('═'.repeat(80) + '\n');\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // ÉTAPE 1 : Créer l'Adapter MySQL\r
+    // ─────────────────────────────────────────────────────\r
+    // L'adapter contient les requêtes SQL spécifiques\r
+    const adapter = createMySQLAdapter(dbConnection, {\r
+        alertsTable: options.alertsTable || 'alert_rules',\r
+        usersTable: options.usersTable || 'users',\r
+        eventsTable: options.eventsTable || 'alert_events'\r
+    });\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // ÉTAPE 2 : Créer le Repository\r
+    // ─────────────────────────────────────────────────────\r
+    // Le repository gère les requêtes SQL\r
+    const alertsRepo = createAlertsRepo(adapter, {\r
+        alertsTable: options.alertsTable || 'alert_rules',\r
+        usersTable: options.usersTable || 'users',\r
+        eventsTable: options.eventsTable || 'alert_events'\r
+    });\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // ÉTAPE 3 : Créer le Service\r
+    // ─────────────────────────────────────────────────────\r
+    // Le service contient la logique métier\r
+    const alertsService = createAlertsService(alertsRepo, {\r
+        defaultCooldown: options.defaultCooldown || 60000\r
+    });\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // ÉTAPE 4 : Créer le Controller\r
+    // ─────────────────────────────────────────────────────\r
+    // Le controller gère les requêtes HTTP\r
+    // (Plus besoin de db car le CRUD passe par le repo)\r
+    const alertsController = createAlertsController(alertsService, alertsRepo);\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // ÉTAPE 5 : Créer le Router\r
+    // ─────────────────────────────────────────────────────\r
+    // Le router définit les routes API\r
+    const alertsRouter = createAlertsRouter(alertsController);\r
+\r
+    console.log('Module Alerts complètement initialisé !');\r
+    console.log('═'.repeat(80) + '\n');\r
+\r
+    // ─────────────────────────────────────────────────────\r
+    // RETOURNER TOUT\r
+    // ─────────────────────────────────────────────────────\r
+    return {\r
+        adapter,\r
+        repo: alertsRepo,\r
+        service: alertsService,\r
+        controller: alertsController,\r
+        router: alertsRouter\r
+    };\r
+}\r
+\r
+// =========================================================\r
+// EXPORT PAR DÉFAUT\r
+// =========================================================\r
+// Pour Wall-e-tte, initialiser avec la DB par défaut\r
+export default initializeAlertsModule();\r
+\r
+// =========================================================\r
+// EXEMPLE D'UTILISATION\r
+// =========================================================\r
+/*\r
+    UTILISATION SIMPLE (dans Wall-e-tte) :\r
+\r
+    // Option 1 : Import par défaut (utilise la DB par défaut)\r
+    import alerts from './modules/init-alerts.js';\r
+\r
+    app.use('/api/alerts', alerts.router);\r
+    await alerts.service.processSignal(signal);\r
+\r
+================================================================================\r
+\r
+    // Option 2 : Import avec configuration personnalisée\r
+    import { initializeAlertsModule } from './modules/init-alerts.js';\r
+    import myDb from './my-db.js';\r
+\r
+    const alerts = initializeAlertsModule(myDb, {\r
+        alertsTable: 'mes_alertes',\r
+        defaultCooldown: 120000  // 2 minutes\r
+    });\r
+\r
+    app.use('/api/alerts', alerts.router);\r
+\r
+================================================================================\r
+\r
+    UTILISATION DANS UN AUTRE PROJET :\r
+\r
+    // Copier ce fichier et changer juste l'import de 'db'\r
+    import strangerDb from './stranger-db-config.js';\r
+    import {\r
+        createAlertsRepo,\r
+        createAlertsService\r
+    } from '@wall-e-tte/alerts';\r
+\r
+    const repo = createAlertsRepo(strangerDb);\r
+    const service = createAlertsService(repo);\r
+\r
+*/\r