]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification 'server' mis das 'Wallette'
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Tue, 24 Feb 2026 15:44:15 +0000 (16:44 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Tue, 24 Feb 2026 15:44:15 +0000 (16:44 +0100)
39 files changed:
Wallette/server/.env-thibaud [new file with mode: 0644]
Wallette/server/.gitignore [new file with mode: 0644]
Wallette/server/DOC-FRONTEND-SOCKET_POUR_OCEANE_ET_THIBAUD.md [new file with mode: 0644]
Wallette/server/EXAMPLE-other-project.js [new file with mode: 0644]
Wallette/server/app.js [new file with mode: 0644]
Wallette/server/config/db.js [new file with mode: 0644]
Wallette/server/modules/alerts/adapters/mysql.adapter.js [new file with mode: 0644]
Wallette/server/modules/alerts/alerts.controller.js [new file with mode: 0644]
Wallette/server/modules/alerts/alerts.repo.js [new file with mode: 0644]
Wallette/server/modules/alerts/alerts.router.js [new file with mode: 0644]
Wallette/server/modules/alerts/alerts.service.js [new file with mode: 0644]
Wallette/server/modules/alerts/channels/console.js [new file with mode: 0644]
Wallette/server/modules/alerts/channels/discord.js [new file with mode: 0644]
Wallette/server/modules/alerts/channels/mailer.js [new file with mode: 0644]
Wallette/server/modules/alerts/channels/telegram.js [new file with mode: 0644]
Wallette/server/modules/alerts/channels/web.js [new file with mode: 0644]
Wallette/server/modules/alerts/index.js [new file with mode: 0644]
Wallette/server/modules/alerts/socketManager.js [new file with mode: 0644]
Wallette/server/modules/alerts/test-alerts.js [new file with mode: 0644]
Wallette/server/modules/init-alerts.js [new file with mode: 0644]
Wallette/server/package-lock.json [new file with mode: 0644]
Wallette/server/package.json [new file with mode: 0644]
Wallette/server/test-alerts.js [new file with mode: 0644]
Wallette/server/test-module-complet.js [new file with mode: 0644]
Wallette/server/test-server-socket.js [new file with mode: 0644]
Wallette/server/test-server.js [new file with mode: 0644]
Wallette/system_notification/.gitignore [new file with mode: 0644]
Wallette/system_notification/README.md [new file with mode: 0644]
Wallette/system_notification/config/db.js [new file with mode: 0644]
Wallette/system_notification/package-lock.json [new file with mode: 0644]
Wallette/system_notification/package.json [new file with mode: 0644]
Wallette/system_notification/repositories/MySqlAlertRepository.js [new file with mode: 0644]
Wallette/system_notification/services/alerts/alertServices.js [new file with mode: 0644]
Wallette/system_notification/services/channels/discord.js [new file with mode: 0644]
Wallette/system_notification/services/channels/mailer.js [new file with mode: 0644]
Wallette/system_notification/services/channels/telegram.js [new file with mode: 0644]
Wallette/system_notification/services/channels/web.js [new file with mode: 0644]
Wallette/system_notification/test-connection.js [new file with mode: 0644]
Wallette/system_notification/test-mailer.js [new file with mode: 0644]

diff --git a/Wallette/server/.env-thibaud b/Wallette/server/.env-thibaud
new file mode 100644 (file)
index 0000000..9bf7f12
--- /dev/null
@@ -0,0 +1,6 @@
+DB_HOST=127.0.0.1
+DB_PORT=33020
+DB_USER=root
+DB_PASSWORD=password
+DB_NAME=wallette
+PORT=3000
\ No newline at end of file
diff --git a/Wallette/server/.gitignore b/Wallette/server/.gitignore
new file mode 100644 (file)
index 0000000..713d500
--- /dev/null
@@ -0,0 +1,2 @@
+node_modules/
+.env
diff --git a/Wallette/server/DOC-FRONTEND-SOCKET_POUR_OCEANE_ET_THIBAUD.md b/Wallette/server/DOC-FRONTEND-SOCKET_POUR_OCEANE_ET_THIBAUD.md
new file mode 100644 (file)
index 0000000..5639e1a
--- /dev/null
@@ -0,0 +1,227 @@
+# 📡 Guide Socket.IO - Alertes Temps Réel
+
+## Pour Océane (Web) et Thibaud (Mobile)
+
+Ce document explique comment recevoir les alertes en temps réel depuis le serveur.
+
+---
+
+## 🔧 Installation
+
+### Web (JavaScript/React)
+```bash
+npm install socket.io-client
+```
+
+### Mobile (React Native / Expo)
+```bash
+npm install socket.io-client
+```
+
+---
+
+## Connexion au serveur
+
+### Code TypeScript/JavaScript
+
+```typescript
+import { io, Socket } from 'socket.io-client';
+
+// URL du serveur (à adapter selon l'environnement)
+const SERVER_URL = 'http://localhost:3000';
+
+// Créer la connexion
+const socket: Socket = io(SERVER_URL);
+
+// Événement : connexion établie
+socket.on('connect', () => {
+    console.log('✅ Connecté au serveur Socket.IO');
+
+    // IMPORTANT : S'authentifier avec l'userId
+    const userId = 'user-123'; // Récupérer depuis votre auth
+    socket.emit('auth', userId);
+});
+
+// Événement : authentification confirmée
+socket.on('auth_success', (data) => {
+    console.log('✅ Authentifié:', data.message);
+});
+
+// Événement : déconnexion
+socket.on('disconnect', (reason) => {
+    console.log('❌ Déconnecté:', reason);
+});
+```
+
+---
+
+## Recevoir les alertes
+
+```typescript
+// Structure d'une alerte reçue
+interface Alert {
+    action: 'BUY' | 'SELL' | 'HOLD';
+    pair: string;           // ex: 'BTC/EUR'
+    confidence: number;     // 0 à 1
+    reason: string;         // Explication du signal
+    alertLevel: 'INFO' | 'WARNING' | 'CRITICAL';
+    timestamp: number;      // Unix timestamp ms
+    price?: number;         // Prix au moment du signal
+}
+
+// Écouter les alertes
+socket.on('alert', (alert: Alert) => {
+    console.log('🔔 Nouvelle alerte:', alert);
+
+    // Exemple de traitement
+    if (alert.alertLevel === 'CRITICAL') {
+        // Notification urgente
+        showUrgentNotification(alert);
+    } else {
+        // Notification normale
+        showNotification(alert);
+    }
+});
+```
+
+---
+
+## Tester la connexion
+
+```typescript
+// Envoyer un ping de test
+socket.emit('ping_alerts');
+
+// Recevoir
+socket.on('pong_alerts', (data) => {
+    console.log('Connexion OK, latence:', Date.now() - data.timestamp, 'ms');
+});
+```
+
+---
+
+## Exemple complet React Native (Thibaud)
+
+```typescript
+// services/socketService.ts
+import { io, Socket } from 'socket.io-client';
+import { Alert } from '../types/DashboardSummary';
+
+class SocketService {
+    private socket: Socket | null = null;
+    private listeners: ((alert: Alert) => void)[] = [];
+
+    connect(serverUrl: string, userId: string) {
+        this.socket = io(serverUrl);
+
+        this.socket.on('connect', () => {
+            console.log('Socket connecté');
+            this.socket?.emit('auth', userId);
+        });
+
+        this.socket.on('alert', (alert: Alert) => {
+            this.listeners.forEach(cb => cb(alert));
+        });
+    }
+
+    onAlert(callback: (alert: Alert) => void) {
+        this.listeners.push(callback);
+    }
+
+    disconnect() {
+        this.socket?.disconnect();
+        this.socket = null;
+    }
+}
+
+export const socketService = new SocketService();
+```
+
+```typescript
+// Dans un composant React Native
+import { useEffect, useState } from 'react';
+import { socketService } from '../services/socketService';
+
+export function useLiveAlerts(userId: string) {
+    const [alerts, setAlerts] = useState<Alert[]>([]);
+
+    useEffect(() => {
+        socketService.connect('http://YOUR_SERVER:3000', userId);
+
+        socketService.onAlert((alert) => {
+            setAlerts(prev => [alert, ...prev]);
+        });
+
+        return () => socketService.disconnect();
+    }, [userId]);
+
+    return alerts;
+}
+```
+
+---
+
+## Exemple complet React Web (Océane)
+
+```typescript
+// hooks/useSocket.ts
+import { useEffect, useState } from 'react';
+import { io, Socket } from 'socket.io-client';
+
+interface Alert {
+    action: 'BUY' | 'SELL' | 'HOLD';
+    pair: string;
+    confidence: number;
+    reason: string;
+    alertLevel: 'INFO' | 'WARNING' | 'CRITICAL';
+    timestamp: number;
+    price?: number;
+}
+
+export function useSocket(serverUrl: string, userId: string) {
+    const [socket, setSocket] = useState<Socket | null>(null);
+    const [connected, setConnected] = useState(false);
+    const [alerts, setAlerts] = useState<Alert[]>([]);
+
+    useEffect(() => {
+        const newSocket = io(serverUrl);
+
+        newSocket.on('connect', () => {
+            setConnected(true);
+            newSocket.emit('auth', userId);
+        });
+
+        newSocket.on('disconnect', () => {
+            setConnected(false);
+        });
+
+        newSocket.on('alert', (alert: Alert) => {
+            setAlerts(prev => [alert, ...prev].slice(0, 50)); // Garder 50 max
+        });
+
+        setSocket(newSocket);
+
+        return () => {
+            newSocket.disconnect();
+        };
+    }, [serverUrl, userId]);
+
+    return { socket, connected, alerts };
+}
+```
+
+---
+
+## Récapitulatif des événements
+
+| Event | Direction | Description |
+|-------|-----------|-------------|
+| `auth` | Client → Serveur | Envoyer l'userId pour s'authentifier |
+| `auth_success` | Serveur → Client | Confirmation d'authentification |
+| `alert` | Serveur → Client | Réception d'une alerte |
+| `ping_alerts` | Client → Serveur | Test de connexion |
+| `pong_alerts` | Serveur → Client | Réponse au ping |
+
+---
+
+
diff --git a/Wallette/server/EXAMPLE-other-project.js b/Wallette/server/EXAMPLE-other-project.js
new file mode 100644 (file)
index 0000000..0538d04
--- /dev/null
@@ -0,0 +1,125 @@
+// =========================================================
+// EXEMPLE : Utilisation du module dans UN AUTRE PROJET
+// =========================================================
+// Ce fichier montre comment un projet DIFFÉRENT de Wall-e-tte
+// peut utiliser le module d'alertes
+// =========================================================
+
+/*
+    CONTEXTE :
+    ton module d'alertes.
+
+    Leur projet a :
+    - Une DB avec des tables différentes
+    - Une structure différente
+    - Mais ils veulent ton système d'alertes !
+*/
+
+// =========================================================
+// 1. LEUR CONFIGURATION DB (différente de Wall-e-tte)
+// =========================================================
+import mysql from 'mysql2/promise';
+
+// Leur connexion DB (avec leurs credentials)
+const theirDb = mysql.createPool({
+    host: 'their-server.com',
+    user: 'their-user',
+    password: 'their-password',
+    database: 'crypto_tracker_db'
+});
+
+// =========================================================
+// 2. IMPORT DU MODULE ALERTS
+// =========================================================
+// Ils peuvent installer via npm ou copier les fichiers
+// import { createAlertsRepo, createAlertsService } from '@wall-e-tte/alerts-module';
+
+// Ou si les fichiers sont copiés dans leur projet :
+import { createAlertsRepo } from './vendors/wall-e-tte-alerts/alerts.repo.js';
+import { createAlertsService } from './vendors/wall-e-tte-alerts/alerts.service.js';
+
+// =========================================================
+// 3. INITIALISATION AVEC LEURS TABLES
+// =========================================================
+const alertsRepo = createAlertsRepo(theirDb, {
+    alertsTable: 'notification_rules',      // ← Leur nom
+    usersTable: 'accounts',                 // ← Leur nom
+    eventsTable: 'notification_history'     // ← Leur nom
+});
+
+const alertsService = createAlertsService(alertsRepo);
+
+// =========================================================
+// 4. UTILISATION - EXACTEMENT PAREIL !
+// =========================================================
+async function handleCryptoSignal() {
+
+    // Leur signal (même structure que Wall-e-tte)
+    const signal = {
+        userId: 'account-789',              // Leur ID format
+        pairId: 42,                          // Leur ID format
+        pair: 'ETH/USD',
+        action: 'SELL',
+        confidence: 0.92,
+        criticality: 'CRITICAL',
+        reason: 'Major resistance level reached, volume increasing'
+    };
+
+    //Le module utilise LEURS tables
+    await alertsService.processSignal(signal);
+
+    console.log('Signal traité avec succès dans CryptoTracker !');
+}
+
+// =========================================================
+// 5. STRUCTURE DE TABLE (différente de Wall-e-tte)
+// =========================================================
+/*
+    CREATE TABLE notification_rules (
+        rule_id VARCHAR(36) PRIMARY KEY,
+        user_id VARCHAR(36),              -- Pointent vers 'accounts'
+        enabled TINYINT(1),
+        severity ENUM('CRITICAL','WARNING','INFO'),
+        min_confidence DECIMAL(5,4),
+        channel ENUM('WEB','CONSOLE','EMAIL','DISCORD','TELEGRAM'),
+        cooldown_ms BIGINT,
+        last_notified_at_ms BIGINT,
+        -- ... autres colonnes
+        FOREIGN KEY (user_id) REFERENCES accounts(user_id)
+    );
+
+    CREATE TABLE accounts (
+        user_id VARCHAR(36) PRIMARY KEY,
+        email VARCHAR(255),
+        -- ... autres colonnes
+    );
+
+    CREATE TABLE notification_history (
+        alert_event_id VARCHAR(36) PRIMARY KEY,
+        rule_id VARCHAR(36),
+        timestamp_ms BIGINT,
+        severity ENUM('CRITICAL','WARNING','INFO'),
+        channel VARCHAR(20),
+        send_status ENUM('PENDING','SENT','FAILED'),
+        -- ... autres colonnes
+    );
+*/
+
+// =========================================================
+// RÉSULTAT :
+// =========================================================
+/*
+    Le module marche dans d'autres projets
+    pas de mondif du code
+    Il y aura juste à configurr les noms de tables
+*/
+
+// =========================================================
+// EXPORT POUR UTILISATION DANS LEUR APP
+// =========================================================
+export { alertsRepo, alertsService };
+export default alertsService;
+
+// Dans leur app :
+// import alertsService from './alerts-integration.js';
+// await alertsService.processSignal(signal);
diff --git a/Wallette/server/app.js b/Wallette/server/app.js
new file mode 100644 (file)
index 0000000..91b3589
--- /dev/null
@@ -0,0 +1,399 @@
+// =========================================================
+// WALL-E-TTE - SERVEUR PRINCIPAL
+// =========================================================
+// Point d'entrée de l'application
+// Intègre tous les modules : Alerts, (Stratégie, Prix, etc.)
+// =========================================================
+// USAGE : node app.js
+// =========================================================
+
+import cors from 'cors';
+import dotenv from 'dotenv';
+import express from 'express';
+import { createServer } from 'http';
+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') });
+
+// =========================================================
+// IMPORTS DES MODULES
+// =========================================================
+
+// Module Alerts (Stéphane)
+import db from './config/db.js';
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
+import { createAlertsController } from './modules/alerts/alerts.controller.js';
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
+import { createAlertsRouter } from './modules/alerts/alerts.router.js';
+import { createAlertsService } from './modules/alerts/alerts.service.js';
+import { broadcastAlert, getConnectedUsersCount, initSocketIO } from './modules/alerts/socketManager.js';
+
+// ─────────────────────────────────────────────────────────
+// TODO: Ajouter les autres modules ici
+// ─────────────────────────────────────────────────────────
+// Module Stratégie (Sacha)
+// import { initializeStrategyModule } from './modules/strategy/index.js';
+
+// Module Prix (Valentin)
+// import { initializePriceModule } from './modules/price/index.js';
+
+// Module Auth (si existant)
+// import { initializeAuthModule } from './modules/auth/index.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+
+// Middlewares
+app.use(cors());                    // Autoriser les requêtes cross-origin
+app.use(express.json());            // Parser le JSON
+app.use(express.urlencoded({ extended: true }));
+
+// =========================================================
+// CRÉER LE SERVEUR HTTP (nécessaire pour Socket.IO)
+// =========================================================
+const httpServer = createServer(app);
+
+// =========================================================
+// INITIALISER SOCKET.IO
+// =========================================================
+const io = initSocketIO(httpServer, {
+    cors: {
+        origin: "*",  // En prod, restreindre aux domaines autorisés
+        methods: ["GET", "POST"]
+    }
+});
+
+console.log('✅ Socket.IO initialisé');
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS (Stéphane)
+// =========================================================
+const alertsAdapter = createMySQLAdapter(db);
+const alertsRepo = createAlertsRepo(alertsAdapter);
+const alertsService = createAlertsService(alertsRepo);
+const alertsController = createAlertsController(alertsService, alertsRepo);
+const alertsRouter = createAlertsRouter(alertsController);
+
+console.log('✅ Module Alerts initialisé');
+
+// =========================================================
+// MONTER LES ROUTES API
+// =========================================================
+
+// Routes Alerts
+app.use('/api/alerts', alertsRouter);
+
+// ─────────────────────────────────────────────────────────
+// TODO: Monter les autres routes ici
+// ─────────────────────────────────────────────────────────
+// app.use('/api/strategy', strategyRouter);
+// app.use('/api/price', priceRouter);
+// app.use('/api/auth', authRouter);
+
+// =========================================================
+// ROUTE HEALTH CHECK
+// =========================================================
+app.get('/', (req, res) => {
+    res.json({
+        name: 'Wall-e-tte API',
+        status: 'running',
+        version: '1.0.0',
+        socketConnections: getConnectedUsersCount(),
+        timestamp: new Date().toISOString(),
+        endpoints: {
+            alerts: '/api/alerts',
+            // strategy: '/api/strategy',
+            // price: '/api/price',
+        }
+    });
+});
+
+// =========================================================
+// PAGE DE TEST SOCKET.IO
+// =========================================================
+app.get('/test', (req, res) => {
+    res.send(`<!DOCTYPE html>
+<html lang="fr">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Wall-e-tte - Test Socket.IO</title>
+    <script src="/socket.io/socket.io.js"></script>
+    <style>
+        * { box-sizing: border-box; }
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: #1a1a2e;
+            color: #eee;
+            padding: 20px;
+            max-width: 800px;
+            margin: 0 auto;
+        }
+        h1 { color: #00d4ff; }
+        .status {
+            padding: 10px 20px;
+            border-radius: 8px;
+            margin: 10px 0;
+            font-weight: bold;
+        }
+        .connected { background: #00c853; color: #000; }
+        .disconnected { background: #ff5252; }
+        .authenticated { background: #00bcd4; color: #000; }
+
+        #alerts {
+            background: #16213e;
+            border-radius: 8px;
+            padding: 15px;
+            max-height: 400px;
+            overflow-y: auto;
+        }
+        .alert-item {
+            background: #0f3460;
+            border-left: 4px solid #00d4ff;
+            padding: 10px 15px;
+            margin: 10px 0;
+            border-radius: 0 8px 8px 0;
+        }
+        .alert-item.WARNING { border-left-color: #ffc107; }
+        .alert-item.CRITICAL { border-left-color: #ff5252; }
+        .alert-item.INFO { border-left-color: #00d4ff; }
+
+        .alert-action {
+            font-size: 1.5em;
+            font-weight: bold;
+        }
+        .alert-action.BUY { color: #00c853; }
+        .alert-action.SELL { color: #ff5252; }
+        .alert-action.HOLD { color: #ffc107; }
+
+        button {
+            background: #00d4ff;
+            color: #000;
+            border: none;
+            padding: 10px 20px;
+            border-radius: 8px;
+            cursor: pointer;
+            font-size: 1em;
+            margin: 5px;
+        }
+        button:hover { background: #00a0c0; }
+
+        input {
+            background: #16213e;
+            border: 1px solid #00d4ff;
+            color: #fff;
+            padding: 10px;
+            border-radius: 8px;
+            margin: 5px;
+        }
+
+        #log {
+            background: #000;
+            color: #0f0;
+            font-family: monospace;
+            padding: 15px;
+            border-radius: 8px;
+            max-height: 200px;
+            overflow-y: auto;
+            font-size: 12px;
+        }
+    </style>
+</head>
+<body>
+    <h1> Wall-e-tte - Test Socket.IO</h1>
+
+    <div id="connection-status" class="status disconnected">
+         Déconnecté
+    </div>
+
+    <div id="auth-status" class="status" style="display: none;">
+    </div>
+
+    <div style="margin: 20px 0;">
+        <input type="text" id="userId" placeholder="User ID" value="test-user">
+        <button onclick="authenticate()">S'authentifier</button>
+        <button onclick="sendTestAlert()">Envoyer alerte test</button>
+    </div>
+
+    <h2> Alertes reçues</h2>
+    <div id="alerts">
+        <em>En attente d'alertes...</em>
+    </div>
+
+    <h2> Log</h2>
+    <div id="log"></div>
+
+    <script>
+        const statusEl = document.getElementById('connection-status');
+        const authStatusEl = document.getElementById('auth-status');
+        const alertsEl = document.getElementById('alerts');
+        const logEl = document.getElementById('log');
+
+        let alertCount = 0;
+
+        function log(message) {
+            const time = new Date().toLocaleTimeString();
+            logEl.innerHTML = '[' + time + '] ' + message + '<br>' + logEl.innerHTML;
+            console.log(message);
+        }
+
+        // Connexion Socket.IO
+        const socket = io();
+
+        socket.on('connect', () => {
+            log(' Connecté au serveur Socket.IO');
+            statusEl.className = 'status connected';
+            statusEl.textContent = ' Connecté (ID: ' + socket.id + ')';
+        });
+
+        socket.on('disconnect', (reason) => {
+            log(' Déconnecté: ' + reason);
+            statusEl.className = 'status disconnected';
+            statusEl.textContent = '❌ Déconnecté';
+            authStatusEl.style.display = 'none';
+        });
+
+        socket.on('auth_success', (data) => {
+            log(' Authentifié: ' + data.message);
+            authStatusEl.style.display = 'block';
+            authStatusEl.className = 'status authenticated';
+            authStatusEl.textContent = ' Authentifié: ' + data.userId;
+        });
+
+        socket.on('alert', (alert) => {
+            alertCount++;
+            log(' Alerte reçue: ' + alert.action + ' ' + alert.pair);
+
+            if (alertCount === 1) {
+                alertsEl.innerHTML = '';
+            }
+
+            const alertHtml =
+                '<div class="alert-item ' + alert.alertLevel + '">' +
+                    '<div class="alert-action ' + alert.action + '">' + alert.action + '</div>' +
+                    '<div><strong>' + alert.pair + '</strong></div>' +
+                    '<div>Confiance: ' + Math.round(alert.confidence * 100) + '%</div>' +
+                    '<div>Niveau: ' + alert.alertLevel + '</div>' +
+                    '<div>' + alert.reason + '</div>' +
+                    '<div><small>' + new Date(alert.timestamp).toLocaleString() + '</small></div>' +
+                '</div>';
+
+            alertsEl.innerHTML = alertHtml + alertsEl.innerHTML;
+        });
+
+        function authenticate() {
+            const userId = document.getElementById('userId').value;
+            if (userId) {
+                socket.emit('auth', userId);
+                log(' Envoi auth pour: ' + userId);
+            }
+        }
+
+        async function sendTestAlert() {
+            try {
+                const response = await fetch('/test/broadcast-alert', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({})
+                });
+                const result = await response.json();
+                log(' Alerte broadcast: ' + result.message);
+            } catch (error) {
+                log(' Erreur: ' + error.message);
+            }
+        }
+
+        // Auto-auth au chargement
+        socket.on('connect', () => {
+            setTimeout(authenticate, 500);
+        });
+    </script>
+</body>
+</html>`);
+});
+
+// =========================================================
+// ROUTE DE TEST : SIMULER UNE ALERTE
+// =========================================================
+app.post('/test/broadcast-alert', (req, res) => {
+    const testAlert = req.body.alert || {
+        action: 'BUY',
+        pair: 'BTC/EUR',
+        confidence: 0.87,
+        reason: 'Signal de test',
+        alertLevel: 'WARNING',
+        price: 42150.23
+    };
+
+    const count = broadcastAlert(testAlert);
+
+    res.json({
+        success: true,
+        message: `Alerte envoyée à ${count} utilisateur(s) connecté(s)`,
+        alert: testAlert
+    });
+});
+
+// =========================================================
+// GESTION DES ERREURS 404
+// =========================================================
+app.use((req, res) => {
+    res.status(404).json({
+        error: 'Route non trouvée',
+        path: req.path
+    });
+});
+
+// =========================================================
+// GESTION DES ERREURS GLOBALES
+// =========================================================
+app.use((err, req, res, next) => {
+    console.error(' Erreur serveur:', err);
+    res.status(500).json({
+        error: 'Erreur serveur',
+        message: process.env.NODE_ENV === 'development' ? err.message : undefined
+    });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+httpServer.listen(PORT, () => {
+    console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║               WALL-E-TTE SERVER                         ║
+╠═══════════════════════════════════════════════════════════╣
+║                                                           ║
+║  HTTP    : http://localhost:${PORT}                         ║
+║  Socket  : ws://localhost:${PORT}                           ║
+║                                                           ║
+╠═══════════════════════════════════════════════════════════╣
+║  MODULES ACTIFS :                                         ║
+║   Alerts (Stéphane)    → /api/alerts                    ║
+║   Strategy (Sacha)     → À intégrer                     ║
+║   Price (Valentin)     → À intégrer                     ║
+║                                                           ║
+╠═══════════════════════════════════════════════════════════╣
+║  SOCKET.IO EVENTS :                                       ║
+║  → Client envoie 'auth' avec userId                       ║
+║  → Client reçoit 'alert' quand signal détecté             ║
+║                                                           ║
+╚═══════════════════════════════════════════════════════════╝
+`);
+});
+
+// =========================================================
+// EXPORT POUR LES AUTRES MODULES
+// =========================================================
+// Les autres modules peuvent importer le service alerts
+// pour envoyer des notifications
+export { alertsRepo, alertsService, io };
+
diff --git a/Wallette/server/config/db.js b/Wallette/server/config/db.js
new file mode 100644 (file)
index 0000000..fd3c348
--- /dev/null
@@ -0,0 +1,62 @@
+// =========================================================
+// CONFIGURATION DE LA BASE DE DONNÉES
+// =========================================================
+// Crée une connexion réutilisable à MySQL
+// via un pool de connexions (mysql2/promise)
+// =========================================================
+
+import dotenv from "dotenv";
+import mysql from "mysql2/promise";
+import path from "path";
+import { fileURLToPath } from "url";
+
+// =========================================================
+// CONFIGURATION DU CHEMIN
+// =========================================================
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// =========================================================
+// CHARGEMENT DES VARIABLES D'ENVIRONNEMENT
+// =========================================================
+// .env doit être dans server/.env
+dotenv.config({ path: path.resolve(__dirname, "../.env") });
+
+// =========================================================
+// VALIDATION MINIMALE (évite les connexions "vides")
+// =========================================================
+const DB_HOST = process.env.DB_HOST || "127.0.0.1";
+const DB_PORT = Number(process.env.DB_PORT || 3306);
+const DB_USER = process.env.DB_USER || "root";
+const DB_PASSWORD = process.env.DB_PASSWORD || ""; // ✅ bon nom
+const DB_NAME = process.env.DB_NAME || "wallette"; // ✅ bon nom / pas de typo
+
+// =========================================================
+// CRÉATION DU POOL DE CONNEXIONS
+// =========================================================
+const db = mysql.createPool({
+  host: DB_HOST,
+  port: DB_PORT,
+  user: DB_USER,
+  password: DB_PASSWORD,
+  database: DB_NAME,
+
+  waitForConnections: true,
+  connectionLimit: 10,
+  queueLimit: 0,
+});
+
+// =========================================================
+// TEST DE CONNEXION
+// =========================================================
+db.getConnection()
+  .then((connection) => {
+    console.log("✅ Connexion à la base de données réussie");
+    connection.release();
+  })
+  .catch((err) => {
+    console.error("❌ Erreur de connexion à la base de données:", err.message);
+    console.error("👉 Vérifie ton fichier server/.env (host/port/user/password/db)");
+  });
+
+export default db;
\ No newline at end of file
diff --git a/Wallette/server/modules/alerts/adapters/mysql.adapter.js b/Wallette/server/modules/alerts/adapters/mysql.adapter.js
new file mode 100644 (file)
index 0000000..7236df1
--- /dev/null
@@ -0,0 +1,341 @@
+// =========================================================
+// 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;
+            }
+        },
+    };
+}
+
diff --git a/Wallette/server/modules/alerts/alerts.controller.js b/Wallette/server/modules/alerts/alerts.controller.js
new file mode 100644 (file)
index 0000000..ea02247
--- /dev/null
@@ -0,0 +1,367 @@
+// =========================================================
+// 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
+                });
+            }
+        }
+    };
+}
+
diff --git a/Wallette/server/modules/alerts/alerts.repo.js b/Wallette/server/modules/alerts/alerts.repo.js
new file mode 100644 (file)
index 0000000..8c0e288
--- /dev/null
@@ -0,0 +1,141 @@
+// =========================================================
+// 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);
+        },
+    };
+}
diff --git a/Wallette/server/modules/alerts/alerts.router.js b/Wallette/server/modules/alerts/alerts.router.js
new file mode 100644 (file)
index 0000000..8280d86
--- /dev/null
@@ -0,0 +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;
+}
+
diff --git a/Wallette/server/modules/alerts/alerts.service.js b/Wallette/server/modules/alerts/alerts.service.js
new file mode 100644 (file)
index 0000000..4280614
--- /dev/null
@@ -0,0 +1,323 @@
+// =========================================================
+// 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
+            }
+        }
+    };
+}
diff --git a/Wallette/server/modules/alerts/channels/console.js b/Wallette/server/modules/alerts/channels/console.js
new file mode 100644 (file)
index 0000000..c459871
--- /dev/null
@@ -0,0 +1,233 @@
+// =========================================================
+// 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);
+}
+
diff --git a/Wallette/server/modules/alerts/channels/discord.js b/Wallette/server/modules/alerts/channels/discord.js
new file mode 100644 (file)
index 0000000..f3491d8
--- /dev/null
@@ -0,0 +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';
+
+}
diff --git a/Wallette/server/modules/alerts/channels/mailer.js b/Wallette/server/modules/alerts/channels/mailer.js
new file mode 100644 (file)
index 0000000..1a4ebeb
--- /dev/null
@@ -0,0 +1,226 @@
+// =========================================================
+// 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';
+}
+
diff --git a/Wallette/server/modules/alerts/channels/telegram.js b/Wallette/server/modules/alerts/channels/telegram.js
new file mode 100644 (file)
index 0000000..ce3a74e
--- /dev/null
@@ -0,0 +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';
+}
+
diff --git a/Wallette/server/modules/alerts/channels/web.js b/Wallette/server/modules/alerts/channels/web.js
new file mode 100644 (file)
index 0000000..d70c149
--- /dev/null
@@ -0,0 +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);
+}
diff --git a/Wallette/server/modules/alerts/index.js b/Wallette/server/modules/alerts/index.js
new file mode 100644 (file)
index 0000000..18f74ca
--- /dev/null
@@ -0,0 +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 !
+*/
diff --git a/Wallette/server/modules/alerts/socketManager.js b/Wallette/server/modules/alerts/socketManager.js
new file mode 100644 (file)
index 0000000..ef6ff14
--- /dev/null
@@ -0,0 +1,194 @@
+// =========================================================
+// 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;
+}
diff --git a/Wallette/server/modules/alerts/test-alerts.js b/Wallette/server/modules/alerts/test-alerts.js
new file mode 100644 (file)
index 0000000..0ad76cc
--- /dev/null
@@ -0,0 +1,164 @@
+// =========================================================
+// 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();
diff --git a/Wallette/server/modules/init-alerts.js b/Wallette/server/modules/init-alerts.js
new file mode 100644 (file)
index 0000000..4f927b9
--- /dev/null
@@ -0,0 +1,134 @@
+// =========================================================
+// 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);
+
+*/
diff --git a/Wallette/server/package-lock.json b/Wallette/server/package-lock.json
new file mode 100644 (file)
index 0000000..4ca2e4c
--- /dev/null
@@ -0,0 +1,1238 @@
+{
+  "name": "wall-e-tte-server",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "wall-e-tte-server",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "cors": "^2.8.5",
+        "dotenv": "^16.4.5",
+        "express": "^4.18.2",
+        "mysql2": "^3.6.5",
+        "nodemailer": "^8.0.1",
+        "socket.io": "^4.8.3",
+        "uuid": "^9.0.1"
+      }
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.19",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "25.3.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
+      "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "license": "MIT",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.4",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+      "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.14.0",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/denque": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/engine.io": {
+      "version": "6.6.5",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
+      "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.7.2",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.18.3"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+      "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.3",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.14.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+      "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/mysql2": {
+      "version": "3.17.0",
+      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.0.tgz",
+      "integrity": "sha512-qF1KYPuytBGqAnMzaQ5/rW90iIqcjnrnnS7bvcJcdarJzlUTAiD9ZC0T7mwndacECseSQ6LcRbRvryXLp25m+g==",
+      "license": "MIT",
+      "dependencies": {
+        "aws-ssl-profiles": "^1.1.2",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.7.2",
+        "long": "^5.3.2",
+        "lru.min": "^1.1.3",
+        "named-placeholders": "^1.1.6",
+        "seq-queue": "^0.0.5",
+        "sql-escaper": "^1.3.1"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/mysql2/node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/named-placeholders": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+      "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+      "license": "MIT",
+      "dependencies": {
+        "lru.min": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/nodemailer": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+      "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+      "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/seq-queue": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+      "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/socket.io": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+      "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io": "~6.6.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+      "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "~4.4.1",
+        "ws": "~8.18.3"
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+      "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/sql-escaper": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.1.tgz",
+      "integrity": "sha512-GLMJGWKzrr7BS5E5+8Prix6RGfBd4UokKMxkPSg313X0TvUyjdJU3Xg7FAhhcba4dHnLy81t4YeHETKLGVsDow==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=2.0.0",
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/Wallette/server/package.json b/Wallette/server/package.json
new file mode 100644 (file)
index 0000000..1c28eb2
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "name": "wall-e-tte-server",
+  "version": "1.0.0",
+  "description": "Serveur Wall-e-tte - Trading Crypto Advisor",
+  "type": "module",
+  "main": "app.js",
+  "scripts": {
+    "start": "node app.js",
+    "dev": "node --watch app.js",
+    "test": "node test-module-complet.js",
+    "test:server": "node test-server-socket.js"
+  },
+  "keywords": [
+    "crypto",
+    "trading",
+    "alerts",
+    "wall-e-tte"
+  ],
+  "author": "Équipe Wall-e-tte",
+  "license": "ISC",
+  "dependencies": {
+    "cors": "^2.8.5",
+    "dotenv": "^16.4.5",
+    "express": "^4.18.2",
+    "mysql2": "^3.6.5",
+    "nodemailer": "^8.0.1",
+    "socket.io": "^4.8.3",
+    "uuid": "^9.0.1"
+  }
+}
diff --git a/Wallette/server/test-alerts.js b/Wallette/server/test-alerts.js
new file mode 100644 (file)
index 0000000..2463776
--- /dev/null
@@ -0,0 +1,185 @@
+// =========================================================
+// 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 (nouvelle architecture !)
+// =========================================================
+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 (plus de SQL dans le repo !)
+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('📊 ÉTAPE 1 : 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');
+            console.log('   Pour tester complètement:');
+            console.log('   1. Créer un utilisateur dans la table users');
+            console.log('   2. Créer une règle dans la table alert_rules\n');
+            console.log('   Exemple de SQL :');
+            console.log(`
+                            INSERT INTO users (user_id, email, password_hash, display_name,
+                                                notify_on_hold, min_confidence_notify,
+                                                created_at_ms, updated_at_ms)
+                            VALUES ('test-user', 'test@example.com', 'hash', 'Test User',
+                                    0, 0.75,
+                                    UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000);
+
+                            INSERT INTO alert_rules (rule_id, user_id, pair_id, enabled,
+                                                        rule_type, severity, min_confidence,
+                                                        channel, params, cooldown_ms,
+                                                        created_at_ms, updated_at_ms)
+                            VALUES ('test-rule', 'test-user', 1, 1,
+                                    'SIGNAL_THRESHOLD', 'WARNING', 0.70,
+                                    'CONSOLE', '{}', 60000,
+                                    UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000);
+                                        `);
+
+            console.log('\n   tester les canaux !\n');
+        }
+
+        // ─────────────────────────────────────────────────
+        // ÉTAPE 2 : Test du canal CONSOLE
+        // ─────────────────────────────────────────────────
+        console.log('─'.repeat(80));
+        console.log('📺 ÉTAPE 2 : 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 (si configuré)
+        // ─────────────────────────────────────────────────
+        console.log('─'.repeat(80));
+        console.log('📧 ÉTAPE 3 : 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 l\'email, configure tes 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_TO || 'wallette@outlook.be', testSignalEmail);
+
+            if (status === 'SENT') {
+                console.log('✅ Test EMAIL réussi ! Vérifiez la boîte mail.\n');
+            } else {
+                console.log('❌ Test EMAIL échoué. Vérifiez la configuration.\n');
+            }
+        }
+
+        // ─────────────────────────────────────────────────
+        // ÉTAPE 4 : Test du service complet (si règles en DB)
+        // ─────────────────────────────────────────────────
+        if (rules.length > 0) {
+            console.log('─'.repeat(80));
+            console.log('🔄 ÉTAPE 4 : 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 ? '✅' : '⚠️ '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);
+        console.log(`   ${rules.length > 0 ? '✅' : '⚠️ '} 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();
diff --git a/Wallette/server/test-module-complet.js b/Wallette/server/test-module-complet.js
new file mode 100644 (file)
index 0000000..a17d027
--- /dev/null
@@ -0,0 +1,445 @@
+// =========================================================
+// TEST COMPLET DU MODULE ALERTS
+// =========================================================
+// Ce script teste :
+// 1. Connexion à la base de données
+// 2. CRUD des règles (Create, Read, Update, Delete)
+// 3. Canaux de notification
+// 4. Service processSignal
+// =========================================================
+// USAGE : node test-module-complet.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') });
+
+// =========================================================
+// IMPORTS 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';
+
+// =========================================================
+// INITIALISATION
+// =========================================================
+const adapter = createMySQLAdapter(db);
+const repo = createAlertsRepo(adapter);
+const service = createAlertsService(repo);
+
+// Variable pour stocker l'ID de la règle créée
+let testRuleId = null;
+let testUserId = null;
+
+// =========================================================
+// FONCTIONS UTILITAIRES
+// =========================================================
+function printHeader(title) {
+    console.log('\n' + '─'.repeat(60));
+    console.log(`📋 ${title}`);
+    console.log('─'.repeat(60));
+}
+
+function printSuccess(message) {
+    console.log(`   ✅ ${message}`);
+}
+
+function printError(message) {
+    console.log(`   ❌ ${message}`);
+}
+
+function printInfo(message) {
+    console.log(`   ℹ️  ${message}`);
+}
+
+// =========================================================
+// TEST 1 : CONNEXION DB
+// =========================================================
+async function testConnection() {
+    printHeader('TEST 1 : Connexion à la base de données');
+
+    try {
+        const [rows] = await db.execute('SELECT 1 as test');
+        if (rows[0].test === 1) {
+            printSuccess('Connexion DB OK');
+            return true;
+        }
+    } catch (error) {
+        printError(`Connexion échouée : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 1.5 : PRÉPARER UN UTILISATEUR DE TEST
+// =========================================================
+async function prepareTestUser() {
+    printHeader('TEST 1.5 : Préparation utilisateur de test');
+
+    try {
+        // D'abord, chercher un utilisateur existant
+        const [existingUsers] = await db.execute('SELECT user_id FROM users LIMIT 1');
+
+        if (existingUsers.length > 0) {
+            testUserId = existingUsers[0].user_id;
+            printSuccess(`Utilisateur existant trouvé : ${testUserId}`);
+            return true;
+        }
+
+        // Sinon, créer un utilisateur de test
+        printInfo('Aucun utilisateur trouvé, création d\'un utilisateur de test...');
+
+        const { v4: uuidv4 } = await import('uuid');
+        testUserId = 'test-user-' + uuidv4().substring(0, 8);
+
+        await db.execute(`
+            INSERT INTO users (user_id, email, password_hash, display_name, created_at_ms, updated_at_ms)
+            VALUES (?, ?, ?, ?, ?, ?)
+        `, [testUserId, 'test@test.com', 'hash123', 'Test User', Date.now(), Date.now()]);
+
+        printSuccess(`Utilisateur de test créé : ${testUserId}`);
+        return true;
+
+    } catch (error) {
+        printError(`Erreur préparation utilisateur : ${error.message}`);
+        printInfo('Conseil : Exécute le seed.sql pour créer des données de test');
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 2 : CREATE - Créer une règle
+// =========================================================
+async function testCreateRule() {
+    printHeader('TEST 2 : CRUD - Créer une règle (CREATE)');
+
+    if (!testUserId) {
+        printError('Pas d\'utilisateur de test disponible');
+        return false;
+    }
+
+    try {
+        const ruleData = {
+            userId: testUserId,  // Utiliser l'utilisateur existant/créé
+            pairId: 1,
+            channel: 'CONSOLE',
+            minConfidence: 0.75,
+            severity: 'WARNING',
+            ruleType: 'SIGNAL_THRESHOLD',
+            cooldownMs: 60000
+        };
+
+        printInfo(`Création d'une règle pour user: ${ruleData.userId}`);
+
+        const result = await repo.createRule(ruleData);
+
+        if (result && result.ruleId) {
+            testRuleId = result.ruleId;
+            printSuccess(`Règle créée avec ID: ${testRuleId}`);
+            return true;
+        } else {
+            printError('Règle créée mais pas d\'ID retourné');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur création : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 3 : READ - Lire la règle créée
+// =========================================================
+async function testReadRule() {
+    printHeader('TEST 3 : CRUD - Lire la règle (READ)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à lire (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        printInfo(`Lecture de la règle ID: ${testRuleId}`);
+
+        const rule = await repo.findRuleById(testRuleId);
+
+        if (rule) {
+            printSuccess(`Règle trouvée :`);
+            console.log(`      - user_id: ${rule.user_id}`);
+            console.log(`      - channel: ${rule.channel}`);
+            console.log(`      - min_confidence: ${rule.min_confidence}`);
+            console.log(`      - enabled: ${rule.enabled}`);
+            return true;
+        } else {
+            printError('Règle non trouvée');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur lecture : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 4 : UPDATE - Modifier la règle
+// =========================================================
+async function testUpdateRule() {
+    printHeader('TEST 4 : CRUD - Modifier la règle (UPDATE)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à modifier (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        const updates = {
+            minConfidence: 0.90,
+            channel: 'EMAIL',
+            severity: 'CRITICAL'
+        };
+
+        printInfo(`Modification de la règle ID: ${testRuleId}`);
+        printInfo(`Nouvelles valeurs: confidence=0.90, channel=EMAIL, severity=CRITICAL`);
+
+        const updated = await repo.updateRule(testRuleId, updates);
+
+        if (updated) {
+            // Vérifier la modification
+            const rule = await repo.findRuleById(testRuleId);
+
+            // Debug : afficher les valeurs réelles
+            console.log(`      [DEBUG] min_confidence: ${rule.min_confidence} (type: ${typeof rule.min_confidence})`);
+            console.log(`      [DEBUG] channel: ${rule.channel}`);
+            console.log(`      [DEBUG] severity: ${rule.severity}`);
+
+            // Comparaison tolérante (float peut avoir des imprécisions)
+            const confidenceOk = Math.abs(parseFloat(rule.min_confidence) - 0.90) < 0.01;
+            const channelOk = rule.channel === 'EMAIL';
+
+            if (confidenceOk && channelOk) {
+                printSuccess('Règle modifiée et vérifiée');
+                return true;
+            } else {
+                printError(`Modification non appliquée correctement`);
+                printError(`  → confidence OK: ${confidenceOk}, channel OK: ${channelOk}`);
+                return false;
+            }
+        } else {
+            printError('Modification a retourné false');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur modification : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 5 : DELETE - Supprimer la règle
+// =========================================================
+async function testDeleteRule() {
+    printHeader('TEST 5 : CRUD - Supprimer la règle (DELETE)');
+
+    if (!testRuleId) {
+        printError('Pas de règle à supprimer (test CREATE a échoué)');
+        return false;
+    }
+
+    try {
+        printInfo(`Suppression de la règle ID: ${testRuleId}`);
+
+        const deleted = await repo.deleteRule(testRuleId);
+
+        if (deleted) {
+            // Vérifier la suppression
+            const rule = await repo.findRuleById(testRuleId);
+
+            if (!rule) {
+                printSuccess('Règle supprimée et vérifiée');
+                return true;
+            } else {
+                printError('Règle encore présente après suppression');
+                return false;
+            }
+        } else {
+            printError('Suppression a retourné false');
+            return false;
+        }
+    } catch (error) {
+        printError(`Erreur suppression : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 6 : Canal CONSOLE
+// =========================================================
+async function testConsoleChannel() {
+    printHeader('TEST 6 : Canal de notification CONSOLE');
+
+    try {
+        const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
+
+        const testSignal = {
+            action: 'BUY',
+            pair: 'BTC/EUR',
+            confidence: 0.85,
+            criticality: 'WARNING',
+            reason: 'Test du canal console'
+        };
+
+        await sendConsoleAlert(testSignal);
+        printSuccess('Canal CONSOLE fonctionne');
+        return true;
+    } catch (error) {
+        printError(`Erreur canal CONSOLE : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// TEST 7 : Service processSignal (si règles existent)
+// =========================================================
+async function testProcessSignal() {
+    printHeader('TEST 7 : Service processSignal');
+
+    if (!testUserId) {
+        printError('Pas d\'utilisateur de test disponible');
+        return false;
+    }
+
+    try {
+        // D'abord créer une règle pour le test
+        const ruleData = {
+            userId: testUserId,  // Utiliser l'utilisateur existant
+            pairId: 1,
+            channel: 'CONSOLE',
+            minConfidence: 0.70,
+            severity: 'INFO'
+        };
+
+        printInfo('Création d\'une règle temporaire pour le test...');
+        const rule = await repo.createRule(ruleData);
+
+        // Simuler un signal
+        const testSignal = {
+            userId: testUserId,  // Utiliser l'utilisateur existant
+            pairId: 1,
+            pair: 'BTC/EUR',
+            action: 'BUY',
+            confidence: 0.85,
+            criticality: 'WARNING',
+            reason: 'Test du service processSignal'
+        };
+
+        printInfo('Envoi du signal au service...');
+        await service.processSignal(testSignal);
+
+        printSuccess('Service processSignal exécuté sans erreur');
+
+        // Nettoyer : supprimer la règle de test
+        await repo.deleteRule(rule.ruleId);
+        printInfo('Règle temporaire supprimée');
+
+        return true;
+    } catch (error) {
+        printError(`Erreur processSignal : ${error.message}`);
+        return false;
+    }
+}
+
+// =========================================================
+// EXÉCUTION DE TOUS LES TESTS
+// =========================================================
+async function runAllTests() {
+    console.log('\n' + '═'.repeat(60));
+    console.log('🧪 TESTS COMPLETS DU MODULE ALERTS');
+    console.log('═'.repeat(60));
+
+    const results = {
+        connection: false,
+        prepareUser: false,
+        create: false,
+        read: false,
+        update: false,
+        delete: false,
+        console: false,
+        processSignal: false
+    };
+
+    try {
+        // Tests dans l'ordre
+        results.connection = await testConnection();
+
+        if (results.connection) {
+            results.prepareUser = await prepareTestUser();
+
+            if (results.prepareUser) {
+                results.create = await testCreateRule();
+                results.read = await testReadRule();
+                results.update = await testUpdateRule();
+                results.delete = await testDeleteRule();
+                results.processSignal = await testProcessSignal();
+            }
+
+            results.console = await testConsoleChannel();
+        }
+
+    } catch (error) {
+        console.error('\n❌ ERREUR FATALE:', error);
+    }
+
+    // =========================================================
+    // RÉSUMÉ
+    // =========================================================
+    console.log('\n' + '═'.repeat(60));
+    console.log('📊 RÉSUMÉ DES TESTS');
+    console.log('═'.repeat(60));
+
+    const tests = [
+        { name: 'Connexion DB', result: results.connection },
+        { name: 'Préparation User', result: results.prepareUser },
+        { name: 'CRUD - Create', result: results.create },
+        { name: 'CRUD - Read', result: results.read },
+        { name: 'CRUD - Update', result: results.update },
+        { name: 'CRUD - Delete', result: results.delete },
+        { name: 'Canal Console', result: results.console },
+        { name: 'Service processSignal', result: results.processSignal }
+    ];
+
+    let passed = 0;
+    let failed = 0;
+
+    tests.forEach(test => {
+        const icon = test.result ? '✅' : '❌';
+        console.log(`   ${icon} ${test.name}`);
+        if (test.result) passed++;
+        else failed++;
+    });
+
+    console.log('\n' + '─'.repeat(60));
+    console.log(`   Total: ${passed}/${tests.length} tests réussis`);
+
+    if (failed === 0) {
+        console.log('\n   🎉 TOUS LES TESTS SONT PASSÉS !');
+    } else {
+        console.log(`\n   ⚠️  ${failed} test(s) échoué(s)`);
+    }
+
+    console.log('═'.repeat(60) + '\n');
+
+    // Quitter
+    process.exit(failed === 0 ? 0 : 1);
+}
+
+// =========================================================
+// LANCEMENT
+// =========================================================
+runAllTests();
diff --git a/Wallette/server/test-server-socket.js b/Wallette/server/test-server-socket.js
new file mode 100644 (file)
index 0000000..dbac7f0
--- /dev/null
@@ -0,0 +1,169 @@
+// =========================================================
+// SERVEUR DE TEST AVEC SOCKET.IO
+// =========================================================
+// Lance ce serveur pour tester les alertes temps réel
+// avec Thibault (mobile) ou Océane (web)
+// =========================================================
+// USAGE : node test-server-socket.js
+// =========================================================
+
+import express from 'express';
+import { createServer } from 'http';
+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') });
+
+// =========================================================
+// IMPORTS DU MODULE ALERTS
+// =========================================================
+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';
+import { createAlertsController } from './modules/alerts/alerts.controller.js';
+import { createAlertsRouter } from './modules/alerts/alerts.router.js';
+import { initSocketIO, sendAlertToUser, broadcastAlert, getConnectedUsersCount } from './modules/alerts/socketManager.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+app.use(express.json());
+
+// =========================================================
+// CRÉER LE SERVEUR HTTP (nécessaire pour Socket.IO)
+// =========================================================
+const httpServer = createServer(app);
+
+// =========================================================
+// INITIALISER SOCKET.IO
+// =========================================================
+const io = initSocketIO(httpServer, {
+    cors: {
+        origin: "*",  // Accepter toutes les origines pour le test
+        methods: ["GET", "POST"]
+    }
+});
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS
+// =========================================================
+const adapter = createMySQLAdapter(db);
+const repo = createAlertsRepo(adapter);
+const service = createAlertsService(repo);
+const controller = createAlertsController(service, repo);
+const router = createAlertsRouter(controller);
+
+// =========================================================
+// MONTER LES ROUTES
+// =========================================================
+app.use('/api/alerts', router);
+
+// =========================================================
+// ROUTE DE TEST / HEALTH CHECK
+// =========================================================
+app.get('/', (req, res) => {
+    res.json({
+        status: 'ok',
+        message: 'Serveur Wall-e-tte avec Socket.IO',
+        socketConnections: getConnectedUsersCount(),
+        timestamp: new Date().toISOString()
+    });
+});
+
+// =========================================================
+// ROUTE POUR SIMULER UNE ALERTE (test)
+// =========================================================
+app.post('/test/send-alert', (req, res) => {
+    const { userId, broadcast } = req.body;
+
+    const testAlert = {
+        action: 'BUY',
+        pair: 'BTC/EUR',
+        confidence: 0.87,
+        reason: 'Test manuel depuis le serveur',
+        alertLevel: 'WARNING',
+        price: 42150.23
+    };
+
+    if (broadcast) {
+        // Envoyer à tous
+        const count = broadcastAlert(testAlert);
+        res.json({
+            success: true,
+            message: `Alerte envoyée à ${count} utilisateur(s)`
+        });
+    } else if (userId) {
+        // Envoyer à un utilisateur spécifique
+        const sent = sendAlertToUser(userId, testAlert);
+        res.json({
+            success: sent,
+            message: sent ? 'Alerte envoyée' : 'Utilisateur non connecté'
+        });
+    } else {
+        res.status(400).json({
+            error: 'userId ou broadcast requis'
+        });
+    }
+});
+
+// =========================================================
+// ROUTE POUR VOIR LES CONNEXIONS ACTIVES
+// =========================================================
+app.get('/test/connections', (req, res) => {
+    const { getConnectedUserIds } = require('./modules/alerts/socketManager.js');
+    res.json({
+        count: getConnectedUsersCount(),
+        users: getConnectedUserIds ? getConnectedUserIds() : 'N/A'
+    });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+httpServer.listen(PORT, () => {
+    console.log(`
+                ╔═══════════════════════════════════════════════════════════╗
+                ║        SERVEUR WALL-E-TTE AVEC SOCKET.IO                  ║
+                ╠═══════════════════════════════════════════════════════════╣
+                ║                                                           ║
+                ║  HTTP Server  : http://localhost:${PORT}                  ║
+                ║  Socket.IO    : ws://localhost:${PORT}                    ║
+                ║                                                           ║
+                ╠═══════════════════════════════════════════════════════════╣
+                ║  ROUTES API :                                             ║
+                ║  ───────────────────────────────────────────────────────  ║
+                ║  GET    /                     → Health check              ║
+                ║  GET    /test/connections     → Voir qui est connecté     ║
+                ║  POST   /test/send-alert      → Simuler une alerte        ║
+                ║  POST   /api/alerts/rules     → Créer une règle           ║
+                ║  GET    /api/alerts/rules/:id → Lister les règles         ║
+                ║                                                           ║
+                ╠═══════════════════════════════════════════════════════════╣
+                ║  SOCKET.IO EVENTS :                                       ║
+                ║  ───────────────────────────────────────────────────────  ║
+                ║  Client → Serveur :                                       ║
+                ║    • 'auth' (userId)         → S'authentifier             ║
+                ║    • 'ping_alerts'           → Tester la connexion        ║
+                ║                                                           ║
+                ║  Serveur → Client :                                       ║
+                ║    • 'auth_success'          → Authentification OK        ║
+                ║    • 'alert'                 → Réception d'une alerte     ║
+                ║    • 'pong_alerts'           → Réponse au ping            ║
+                ║                                                           ║
+                ╚═══════════════════════════════════════════════════════════╝
+
+
+                Pour tester manuellement :
+                curl -X POST http://localhost:${PORT}/test/send-alert -H "Content-Type: application/json" -d '{"broadcast": true}'
+
+                Ctrl+C pour arrêter.
+`);
+});
diff --git a/Wallette/server/test-server.js b/Wallette/server/test-server.js
new file mode 100644 (file)
index 0000000..45ec6bc
--- /dev/null
@@ -0,0 +1,121 @@
+// =========================================================
+// MINI-SERVEUR DE TEST POUR LES ROUTES API
+// =========================================================
+// Ce script démarre un serveur Express pour tester les
+// routes du module Alerts via Postman, curl ou navigateur
+// =========================================================
+// USAGE : node test-server.js
+// =========================================================
+
+import express from 'express';
+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') });
+
+// =========================================================
+// IMPORTS DU MODULE ALERTS
+// =========================================================
+import { initializeAlertsModule } from './modules/init-alerts.js';
+
+// =========================================================
+// CRÉER L'APPLICATION EXPRESS
+// =========================================================
+const app = express();
+app.use(express.json());
+
+// =========================================================
+// INITIALISER LE MODULE ALERTS
+// =========================================================
+console.log('\n🚀 Initialisation du module Alerts...\n');
+
+// Import dynamique pour éviter l'erreur de connexion DB
+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';
+import { createAlertsController } from './modules/alerts/alerts.controller.js';
+import { createAlertsRouter } from './modules/alerts/alerts.router.js';
+
+const adapter = createMySQLAdapter(db);
+const repo = createAlertsRepo(adapter);
+const service = createAlertsService(repo);
+const controller = createAlertsController(service, repo);
+const router = createAlertsRouter(controller);
+
+// =========================================================
+// MONTER LES ROUTES
+// =========================================================
+app.use('/api/alerts', router);
+
+// =========================================================
+// ROUTE DE TEST / HEALTH CHECK
+// =========================================================
+app.get('/', (req, res) => {
+    res.json({
+        status: 'ok',
+        message: 'Serveur de test Alerts',
+        timestamp: new Date().toISOString(),
+        routes: [
+            'GET  /api/alerts/rules/:userId     - Lister les règles',
+            'GET  /api/alerts/rules/detail/:id  - Détail d\'une règle',
+            'POST /api/alerts/rules             - Créer une règle',
+            'PUT  /api/alerts/rules/:id         - Modifier une règle',
+            'DELETE /api/alerts/rules/:id       - Supprimer une règle',
+            'GET  /api/alerts/history/:userId   - Historique des alertes',
+            'POST /api/alerts/process-signal    - Traiter un signal'
+        ]
+    });
+});
+
+// =========================================================
+// DÉMARRER LE SERVEUR
+// =========================================================
+const PORT = process.env.PORT || 3000;
+
+app.listen(PORT, () => {
+    console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║           🧪 SERVEUR DE TEST - MODULE ALERTS              ║
+╠═══════════════════════════════════════════════════════════╣
+║                                                           ║
+║  Serveur démarré sur : http://localhost:${PORT}             ║
+║                                                           ║
+║  Routes disponibles :                                     ║
+║  ─────────────────────────────────────────────────────── ║
+║  GET    /                              → Health check     ║
+║  GET    /api/alerts/rules/:userId      → Lister règles    ║
+║  GET    /api/alerts/rules/detail/:id   → Détail règle     ║
+║  POST   /api/alerts/rules              → Créer règle      ║
+║  PUT    /api/alerts/rules/:id          → Modifier règle   ║
+║  DELETE /api/alerts/rules/:id          → Supprimer règle  ║
+║  GET    /api/alerts/history/:userId    → Historique       ║
+║  POST   /api/alerts/process-signal     → Traiter signal   ║
+║                                                           ║
+╚═══════════════════════════════════════════════════════════╝
+
+📝 Exemples de tests avec curl :
+
+1. Créer une règle :
+   curl -X POST http://localhost:${PORT}/api/alerts/rules \\
+     -H "Content-Type: application/json" \\
+     -d '{"userId":"test-user","pairId":1,"channel":"CONSOLE","minConfidence":0.8}'
+
+2. Lister les règles :
+   curl http://localhost:${PORT}/api/alerts/rules/test-user
+
+3. Modifier une règle :
+   curl -X PUT http://localhost:${PORT}/api/alerts/rules/RULE_ID \\
+     -H "Content-Type: application/json" \\
+     -d '{"minConfidence":0.9,"enabled":true}'
+
+4. Supprimer une règle :
+   curl -X DELETE http://localhost:${PORT}/api/alerts/rules/RULE_ID
+
+Appuie sur Ctrl+C pour arrêter le serveur.
+`);
+});
diff --git a/Wallette/system_notification/.gitignore b/Wallette/system_notification/.gitignore
new file mode 100644 (file)
index 0000000..cafdff9
--- /dev/null
@@ -0,0 +1,2 @@
+node_modules/
+.expo/
\ No newline at end of file
diff --git a/Wallette/system_notification/README.md b/Wallette/system_notification/README.md
new file mode 100644 (file)
index 0000000..3216bcc
--- /dev/null
@@ -0,0 +1 @@
+# system_notification
diff --git a/Wallette/system_notification/config/db.js b/Wallette/system_notification/config/db.js
new file mode 100644 (file)
index 0000000..38d1546
--- /dev/null
@@ -0,0 +1,24 @@
+import mysql from 'mysql2/promise';
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+// On recrée __dirname qui n'existe pas en mode "module"
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// On charge le .env
+dotenv.config({ path: path.resolve(__dirname, '../.env') });
+
+const db = mysql.createPool({
+    host: process.env.DB_HOST,
+    port: process.env.DB_PORT,
+    user: process.env.DB_USER,
+    password: process.env.DB_PASS,
+    database: process.env.DB_NAME,
+    waitForConnections: true,
+    connectionLimit: 10,
+    queueLimit: 0
+});
+
+export default db;
diff --git a/Wallette/system_notification/package-lock.json b/Wallette/system_notification/package-lock.json
new file mode 100644 (file)
index 0000000..d7d9d4b
--- /dev/null
@@ -0,0 +1,161 @@
+{
+  "name": "system_notification",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "system_notification",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "dotenv": "^17.2.4",
+        "mysql2": "^3.16.3",
+        "nodemailer": "^8.0.1"
+      }
+    },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/denque": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.2.4",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
+      "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+      "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
+    "node_modules/mysql2": {
+      "version": "3.16.3",
+      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.3.tgz",
+      "integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==",
+      "license": "MIT",
+      "dependencies": {
+        "aws-ssl-profiles": "^1.1.2",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.7.2",
+        "long": "^5.3.2",
+        "lru.min": "^1.1.3",
+        "named-placeholders": "^1.1.6",
+        "seq-queue": "^0.0.5",
+        "sqlstring": "^2.3.3"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/named-placeholders": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+      "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+      "license": "MIT",
+      "dependencies": {
+        "lru.min": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/nodemailer": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+      "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/seq-queue": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+      "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+    },
+    "node_modules/sqlstring": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+      "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    }
+  }
+}
diff --git a/Wallette/system_notification/package.json b/Wallette/system_notification/package.json
new file mode 100644 (file)
index 0000000..c123517
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "name": "system_notification",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "",
+  "main": "test-connection.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "dotenv": "^17.2.4",
+    "mysql2": "^3.16.3",
+    "nodemailer": "^8.0.1"
+  }
+}
diff --git a/Wallette/system_notification/repositories/MySqlAlertRepository.js b/Wallette/system_notification/repositories/MySqlAlertRepository.js
new file mode 100644 (file)
index 0000000..edebf2b
--- /dev/null
@@ -0,0 +1,35 @@
+// 1. On importe la connexion à la base de données
+import db from '../config/db.js';
+
+const MySqlAlertRepository = {
+
+    /**
+     * Récupère toutes les règles d'alerte actives avec l'email de l'utilisateur
+     */
+    async findActiveRules() {
+        try {
+        // On prépare la requête SQL avec une jointure (JOIN)
+        // On récupère les colonnes de alert_rules (ar) et l'email de users (u)
+        const sql = `
+            SELECT ar.*, u.email
+            FROM alert_rules ar
+            JOIN users u ON ar.user_id = u.user_id
+            WHERE ar.enabled = 1
+        `;
+
+        // On exécute la requête
+        const [rows] = await db.execute(sql);
+
+        // On retourne les données
+        return rows;
+        } catch (error) {
+        console.error("Erreur dans findActiveRules:", error);
+        throw error; // On laisse le service gérer l'erreur si besoin
+        }
+    }
+
+
+};
+
+// 3. On exporte le repository
+export default MySqlAlertRepository;
diff --git a/Wallette/system_notification/services/alerts/alertServices.js b/Wallette/system_notification/services/alerts/alertServices.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Wallette/system_notification/services/channels/discord.js b/Wallette/system_notification/services/channels/discord.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Wallette/system_notification/services/channels/mailer.js b/Wallette/system_notification/services/channels/mailer.js
new file mode 100644 (file)
index 0000000..b255c95
--- /dev/null
@@ -0,0 +1,32 @@
+import nodemailer from 'nodemailer';
+
+
+export async function sendAlertEmail(to, signal) {
+    const transporter = nodemailer.createTransport({
+        host: process.env.MAIL_HOST,
+        port: parseInt(process.env.MAIL_PORT),
+        auth: {
+            user: process.env.MAIL_USER,
+            pass: process.env.MAIL_PASS
+        }
+    });
+
+    try {
+        const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
+        const mailOptions = {
+            from: `"Wall-e-tte" <${process.env.MAIL_USER}>`,
+            to: to,
+            subject: `🚨 Alerte : ${signal.action} sur ${signal.pair}`,
+            text: `Action : ${signal.action}\nConfiance : ${confidencePercent}%`,
+        };
+
+        const info = await transporter.sendMail(mailOptions);
+        console.log("✅ Email envoyé ! ID:", info.messageId);
+        return 'SENT';
+    } catch (error) {
+        console.error("❌ Erreur d'envoi mail :", error.message);
+        return 'FAILED';
+    }
+}
+
+
diff --git a/Wallette/system_notification/services/channels/telegram.js b/Wallette/system_notification/services/channels/telegram.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Wallette/system_notification/services/channels/web.js b/Wallette/system_notification/services/channels/web.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Wallette/system_notification/test-connection.js b/Wallette/system_notification/test-connection.js
new file mode 100644 (file)
index 0000000..da2b741
--- /dev/null
@@ -0,0 +1,25 @@
+import repository from './repositories/MySqlAlertRepository.js'; // Note le .js obligatoire !
+
+async function test() {
+    console.log("Tentative de connexion à MariaDB...");
+
+    try {
+        const rules = await repository.findActiveRules();
+        console.log("✅ Connexion réussie !");
+        console.log(`📊 Nombre de règles actives trouvées : ${rules.length}`);
+
+        if (rules.length > 0) {
+            console.log("Détail de la première règle :", rules[0]);
+        } else {
+            console.log("La connexion marche, mais la table 'alert_rules' est vide.");
+        }
+
+    } catch (error) {
+        console.error("❌ ÉCHEC du test :");
+        console.error("Détail de l'erreur :", error.message);
+    } finally {
+        process.exit();
+    }
+}
+
+test();
diff --git a/Wallette/system_notification/test-mailer.js b/Wallette/system_notification/test-mailer.js
new file mode 100644 (file)
index 0000000..7f6670c
--- /dev/null
@@ -0,0 +1,50 @@
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import repository from './repositories/MySqlAlertRepository.js';
+import { sendAlertEmail } from './services/channels/mailer.js';
+
+// Configuration nécessaire pour retrouver __dirname en mode "import"
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Chargement des variables d'environnement
+dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+async function runTest() {
+    console.log("🔍 Recherche d'une règle active dans la DB...");
+
+    try {
+        const rules = await repository.findActiveRules();
+
+        if (rules.length === 0) {
+            console.log("⚠️ Aucune règle trouvée. Vérifie tes tables SQL.");
+            return;
+        }
+
+        const rule = rules[0];
+        console.log(`✅ Règle trouvée pour : ${rule.email}`);
+
+        // On simule un signal de test
+        const fakeSignal = {
+            action: 'BUY',
+            pair: 'BTC/EUR',
+            confidence: 0.85,
+            severity: 'WARNING'
+        };
+
+        console.log("✉️ Tentative d'envoi du mail...");
+
+        // Appel direct de la fonction importée (plus de require ici !)
+        const status = await sendAlertEmail(rule.email, fakeSignal);
+
+        console.log(`📊 Résultat de l'envoi : ${status}`);
+
+    } catch (error) {
+        console.error("❌ Erreur pendant le test :", error);
+    } finally {
+        process.exit();
+    }
+}
+
+runTest();